@hivegpt/hiveai-angular 0.0.574 → 0.0.576
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 +542 -278
- 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 -6
- package/esm2015/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.js +85 -54
- package/esm2015/lib/components/voice-agent/services/daily-voice-client.service.js +153 -63
- package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +160 -88
- package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +81 -34
- package/fesm2015/hivegpt-hiveai-angular.js +478 -239
- 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 +7 -1
- 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 +37 -3
- 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 +19 -6
- 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 +5 -12
- package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -813,6 +813,7 @@ 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;
|
|
816
817
|
class WebSocketVoiceClientService {
|
|
817
818
|
constructor() {
|
|
818
819
|
this.ws = null;
|
|
@@ -826,54 +827,100 @@ class WebSocketVoiceClientService {
|
|
|
826
827
|
/** Emits bot transcript updates. */
|
|
827
828
|
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
828
829
|
}
|
|
829
|
-
/**
|
|
830
|
+
/**
|
|
831
|
+
* Connect to signaling WebSocket. No audio over this connection.
|
|
832
|
+
* Resolves when the socket is open; rejects if the connection fails.
|
|
833
|
+
*/
|
|
830
834
|
connect(wsUrl) {
|
|
831
835
|
var _a;
|
|
832
836
|
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
833
|
-
return;
|
|
837
|
+
return Promise.resolve();
|
|
834
838
|
}
|
|
835
839
|
if (this.ws) {
|
|
836
840
|
this.ws.close();
|
|
837
841
|
this.ws = null;
|
|
838
842
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
843
|
+
return new Promise((resolve, reject) => {
|
|
844
|
+
let settled = false;
|
|
845
|
+
const timeout = setTimeout(() => {
|
|
842
846
|
var _a;
|
|
847
|
+
if (settled)
|
|
848
|
+
return;
|
|
849
|
+
settled = true;
|
|
843
850
|
try {
|
|
844
|
-
|
|
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);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
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
|
-
});
|
|
856
|
-
}
|
|
857
|
-
else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'bot_transcript' && typeof msg.text === 'string') {
|
|
858
|
-
this.botTranscriptSubject.next(msg.text);
|
|
859
|
-
}
|
|
851
|
+
(_a = this.ws) === null || _a === void 0 ? void 0 : _a.close();
|
|
860
852
|
}
|
|
861
853
|
catch (_b) {
|
|
862
|
-
|
|
854
|
+
/* ignore */
|
|
863
855
|
}
|
|
864
|
-
};
|
|
865
|
-
this.ws.onerror = () => {
|
|
866
|
-
this.disconnect();
|
|
867
|
-
};
|
|
868
|
-
this.ws.onclose = () => {
|
|
869
856
|
this.ws = null;
|
|
857
|
+
reject(new Error('WebSocket connection timed out'));
|
|
858
|
+
}, WS_CONNECT_TIMEOUT_MS);
|
|
859
|
+
const clear = () => {
|
|
860
|
+
clearTimeout(timeout);
|
|
870
861
|
};
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
+
}
|
|
881
|
+
}
|
|
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
|
+
}
|
|
895
|
+
};
|
|
896
|
+
ws.onerror = () => {
|
|
897
|
+
if (!settled) {
|
|
898
|
+
settled = true;
|
|
899
|
+
clear();
|
|
900
|
+
this.disconnect();
|
|
901
|
+
reject(new Error('WebSocket connection failed'));
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
// After onopen, some environments fire onerror spuriously; closing here can
|
|
905
|
+
// kill the socket before room_created is delivered. Let onclose clean up.
|
|
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'));
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
catch (err) {
|
|
918
|
+
clear();
|
|
919
|
+
console.error('WebSocketVoiceClient: connect failed', err);
|
|
920
|
+
this.ws = null;
|
|
921
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
922
|
+
}
|
|
923
|
+
});
|
|
877
924
|
}
|
|
878
925
|
/** Disconnect and cleanup. */
|
|
879
926
|
disconnect() {
|
|
@@ -895,59 +942,87 @@ WebSocketVoiceClientService.decorators = [
|
|
|
895
942
|
},] }
|
|
896
943
|
];
|
|
897
944
|
|
|
945
|
+
/**
|
|
946
|
+
* Daily.js WebRTC client for voice agent audio.
|
|
947
|
+
* Responsibilities:
|
|
948
|
+
* - Create and manage Daily CallObject
|
|
949
|
+
* - Join Daily room using room_url
|
|
950
|
+
* - Handle mic capture + speaker playback
|
|
951
|
+
* - Bot speaking detection via AnalyserNode on remote track (instant)
|
|
952
|
+
* - User speaking detection via active-speaker-change
|
|
953
|
+
* - Expose speaking$ (bot speaking), userSpeaking$ (user speaking), micMuted$
|
|
954
|
+
* - Expose localStream$ for waveform visualization (AudioAnalyzerService)
|
|
955
|
+
*/
|
|
898
956
|
class DailyVoiceClientService {
|
|
899
957
|
constructor(ngZone) {
|
|
900
958
|
this.ngZone = ngZone;
|
|
901
959
|
this.callObject = null;
|
|
902
960
|
this.localStream = null;
|
|
903
961
|
this.localSessionId = null;
|
|
962
|
+
/** Explicit playback of remote (bot) audio; required in some browsers. */
|
|
904
963
|
this.remoteAudioElement = null;
|
|
964
|
+
/** AnalyserNode-based remote audio monitor for instant bot speaking detection. */
|
|
905
965
|
this.remoteAudioContext = null;
|
|
906
|
-
|
|
966
|
+
/** Poll interval id (~100ms); named historically when RAF was used. */
|
|
967
|
+
this.remoteSpeakingPollId = null;
|
|
907
968
|
this.speakingSubject = new BehaviorSubject(false);
|
|
908
969
|
this.userSpeakingSubject = new BehaviorSubject(false);
|
|
909
|
-
this.micMutedSubject = new BehaviorSubject(
|
|
970
|
+
this.micMutedSubject = new BehaviorSubject(false);
|
|
910
971
|
this.localStreamSubject = new BehaviorSubject(null);
|
|
972
|
+
/** True when bot (remote participant) is the active speaker. */
|
|
911
973
|
this.speaking$ = this.speakingSubject.asObservable();
|
|
974
|
+
/** True when user (local participant) is the active speaker. */
|
|
912
975
|
this.userSpeaking$ = this.userSpeakingSubject.asObservable();
|
|
976
|
+
/** True when mic is muted. */
|
|
913
977
|
this.micMuted$ = this.micMutedSubject.asObservable();
|
|
978
|
+
/** Emits local mic stream for waveform visualization. */
|
|
914
979
|
this.localStream$ = this.localStreamSubject.asObservable();
|
|
915
980
|
}
|
|
916
|
-
|
|
981
|
+
/**
|
|
982
|
+
* Connect to Daily room. Acquires mic first for waveform, then joins with audio.
|
|
983
|
+
* @param roomUrl Daily room URL (from room_created)
|
|
984
|
+
* @param token Optional meeting token
|
|
985
|
+
* @param existingStream Optional pre-acquired mic (avoids a second getUserMedia / extra prompts on some browsers)
|
|
986
|
+
*/
|
|
987
|
+
connect(roomUrl, token, existingStream) {
|
|
917
988
|
return __awaiter(this, void 0, void 0, function* () {
|
|
918
989
|
if (this.callObject) {
|
|
919
990
|
yield this.disconnect();
|
|
920
991
|
}
|
|
921
992
|
try {
|
|
922
|
-
|
|
923
|
-
const stream =
|
|
993
|
+
const hasLiveTrack = !!(existingStream === null || existingStream === void 0 ? void 0 : existingStream.getAudioTracks().some((t) => t.readyState === 'live'));
|
|
994
|
+
const stream = hasLiveTrack
|
|
995
|
+
? existingStream
|
|
996
|
+
: yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
924
997
|
const audioTrack = stream.getAudioTracks()[0];
|
|
925
|
-
if (!audioTrack)
|
|
998
|
+
if (!audioTrack) {
|
|
999
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
926
1000
|
throw new Error('No audio track');
|
|
1001
|
+
}
|
|
927
1002
|
this.localStream = stream;
|
|
928
1003
|
this.localStreamSubject.next(stream);
|
|
1004
|
+
// Create audio-only call object
|
|
1005
|
+
// videoSource: false = no camera, audioSource = our mic track
|
|
929
1006
|
const callObject = Daily.createCallObject({
|
|
930
1007
|
videoSource: false,
|
|
931
1008
|
audioSource: audioTrack,
|
|
932
1009
|
});
|
|
933
1010
|
this.callObject = callObject;
|
|
934
1011
|
this.setupEventHandlers(callObject);
|
|
935
|
-
//
|
|
936
|
-
|
|
937
|
-
this.micMutedSubject.next(true);
|
|
1012
|
+
// Join room; Daily handles playback of remote (bot) audio automatically.
|
|
1013
|
+
// Only pass token when it's a non-empty string (Daily rejects undefined/non-string).
|
|
938
1014
|
const joinOptions = { url: roomUrl };
|
|
939
|
-
if (typeof token === 'string' && token.trim()) {
|
|
1015
|
+
if (typeof token === 'string' && token.trim() !== '') {
|
|
940
1016
|
joinOptions.token = token;
|
|
941
1017
|
}
|
|
942
1018
|
yield callObject.join(joinOptions);
|
|
943
|
-
console.log(`[VoiceDebug]
|
|
1019
|
+
console.log(`[VoiceDebug] Room connected (Daily join complete) — ${new Date().toISOString()}`);
|
|
944
1020
|
const participants = callObject.participants();
|
|
945
1021
|
if (participants === null || participants === void 0 ? void 0 : participants.local) {
|
|
946
1022
|
this.localSessionId = participants.local.session_id;
|
|
947
1023
|
}
|
|
948
|
-
//
|
|
949
|
-
callObject.
|
|
950
|
-
this.micMutedSubject.next(true);
|
|
1024
|
+
// Initial mute state: Daily starts with audio on
|
|
1025
|
+
this.micMutedSubject.next(!callObject.localAudio());
|
|
951
1026
|
}
|
|
952
1027
|
catch (err) {
|
|
953
1028
|
this.cleanup();
|
|
@@ -956,6 +1031,8 @@ class DailyVoiceClientService {
|
|
|
956
1031
|
});
|
|
957
1032
|
}
|
|
958
1033
|
setupEventHandlers(call) {
|
|
1034
|
+
// active-speaker-change: used ONLY for user speaking detection.
|
|
1035
|
+
// Bot speaking is detected by our own AnalyserNode (instant, no debounce).
|
|
959
1036
|
call.on('active-speaker-change', (event) => {
|
|
960
1037
|
this.ngZone.run(() => {
|
|
961
1038
|
var _a;
|
|
@@ -964,20 +1041,23 @@ class DailyVoiceClientService {
|
|
|
964
1041
|
this.userSpeakingSubject.next(false);
|
|
965
1042
|
return;
|
|
966
1043
|
}
|
|
967
|
-
|
|
1044
|
+
const isLocal = peerId === this.localSessionId;
|
|
1045
|
+
this.userSpeakingSubject.next(isLocal);
|
|
968
1046
|
});
|
|
969
1047
|
});
|
|
1048
|
+
// track-started / track-stopped: set up remote audio playback + AnalyserNode monitor.
|
|
970
1049
|
call.on('track-started', (event) => {
|
|
971
1050
|
this.ngZone.run(() => {
|
|
972
|
-
var _a, _b, _c, _d
|
|
1051
|
+
var _a, _b, _c, _d;
|
|
973
1052
|
const p = event === null || event === void 0 ? void 0 : event.participant;
|
|
974
1053
|
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;
|
|
1054
|
+
const track = event === null || event === void 0 ? void 0 : event.track;
|
|
975
1055
|
if (p && !p.local && type === 'audio') {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
this.playRemoteTrack(
|
|
980
|
-
this.monitorRemoteAudio(
|
|
1056
|
+
console.log(`[VoiceDebug] Got audio track from backend (track-started) — readyState=${track === null || track === void 0 ? void 0 : track.readyState}, muted=${track === null || track === void 0 ? void 0 : track.muted} — ${new Date().toISOString()}`);
|
|
1057
|
+
const audioTrack = track !== null && track !== void 0 ? track : (_d = (_c = p.tracks) === null || _c === void 0 ? void 0 : _c.audio) === null || _d === void 0 ? void 0 : _d.track;
|
|
1058
|
+
if (audioTrack && typeof audioTrack === 'object') {
|
|
1059
|
+
this.playRemoteTrack(audioTrack);
|
|
1060
|
+
this.monitorRemoteAudio(audioTrack);
|
|
981
1061
|
}
|
|
982
1062
|
}
|
|
983
1063
|
});
|
|
@@ -996,27 +1076,57 @@ class DailyVoiceClientService {
|
|
|
996
1076
|
call.on('left-meeting', () => {
|
|
997
1077
|
this.ngZone.run(() => this.cleanup());
|
|
998
1078
|
});
|
|
999
|
-
call.on('error', (
|
|
1000
|
-
|
|
1001
|
-
|
|
1079
|
+
call.on('error', (event) => {
|
|
1080
|
+
this.ngZone.run(() => {
|
|
1081
|
+
var _a;
|
|
1082
|
+
console.error('DailyVoiceClient: Daily error', (_a = event === null || event === void 0 ? void 0 : event.errorMsg) !== null && _a !== void 0 ? _a : event);
|
|
1083
|
+
this.cleanup();
|
|
1084
|
+
});
|
|
1002
1085
|
});
|
|
1003
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Play remote (bot) audio track via a dedicated audio element.
|
|
1089
|
+
* Required in many browsers where Daily's internal playback does not output to speakers.
|
|
1090
|
+
*/
|
|
1004
1091
|
playRemoteTrack(track) {
|
|
1005
1092
|
this.stopRemoteAudio();
|
|
1006
1093
|
try {
|
|
1094
|
+
console.log(`[VoiceDebug] playRemoteTrack called — track.readyState=${track.readyState}, track.muted=${track.muted} — ${new Date().toISOString()}`);
|
|
1095
|
+
track.onunmute = () => {
|
|
1096
|
+
console.log(`[VoiceDebug] Remote audio track UNMUTED (audio data arriving) — ${new Date().toISOString()}`);
|
|
1097
|
+
};
|
|
1007
1098
|
const stream = new MediaStream([track]);
|
|
1008
1099
|
const audio = new Audio();
|
|
1009
1100
|
audio.autoplay = true;
|
|
1010
1101
|
audio.srcObject = stream;
|
|
1011
1102
|
this.remoteAudioElement = audio;
|
|
1012
|
-
audio.
|
|
1013
|
-
console.
|
|
1014
|
-
}
|
|
1103
|
+
audio.onplaying = () => {
|
|
1104
|
+
console.log(`[VoiceDebug] Audio element PLAYING (browser started playback) — ${new Date().toISOString()}`);
|
|
1105
|
+
};
|
|
1106
|
+
let firstTimeUpdate = true;
|
|
1107
|
+
audio.ontimeupdate = () => {
|
|
1108
|
+
if (firstTimeUpdate) {
|
|
1109
|
+
firstTimeUpdate = false;
|
|
1110
|
+
console.log(`[VoiceDebug] Audio element first TIMEUPDATE (actual audio output) — ${new Date().toISOString()}`);
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
const p = audio.play();
|
|
1114
|
+
if (p && typeof p.then === 'function') {
|
|
1115
|
+
p.then(() => {
|
|
1116
|
+
console.log(`[VoiceDebug] audio.play() resolved — ${new Date().toISOString()}`);
|
|
1117
|
+
}).catch((err) => {
|
|
1118
|
+
console.warn('DailyVoiceClient: remote audio play failed (may need user gesture)', err);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1015
1121
|
}
|
|
1016
1122
|
catch (err) {
|
|
1017
|
-
console.warn('
|
|
1123
|
+
console.warn('DailyVoiceClient: failed to create remote audio element', err);
|
|
1018
1124
|
}
|
|
1019
1125
|
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Monitor remote audio track energy via AnalyserNode.
|
|
1128
|
+
* Polls at ~10Hz; sufficient for speaking detection vs ~60fps RAF.
|
|
1129
|
+
*/
|
|
1020
1130
|
monitorRemoteAudio(track) {
|
|
1021
1131
|
this.stopRemoteAudioMonitor();
|
|
1022
1132
|
try {
|
|
@@ -1026,81 +1136,108 @@ class DailyVoiceClientService {
|
|
|
1026
1136
|
analyser.fftSize = 256;
|
|
1027
1137
|
source.connect(analyser);
|
|
1028
1138
|
this.remoteAudioContext = ctx;
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1139
|
+
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
1140
|
+
const THRESHOLD = 5;
|
|
1141
|
+
const SILENCE_MS = 1500;
|
|
1142
|
+
const POLL_MS = 100;
|
|
1143
|
+
let lastSoundTime = 0;
|
|
1144
|
+
let isSpeaking = false;
|
|
1145
|
+
this.remoteSpeakingPollId = setInterval(() => {
|
|
1146
|
+
if (!this.remoteAudioContext) {
|
|
1147
|
+
if (this.remoteSpeakingPollId) {
|
|
1148
|
+
clearInterval(this.remoteSpeakingPollId);
|
|
1149
|
+
this.remoteSpeakingPollId = null;
|
|
1150
|
+
}
|
|
1034
1151
|
return;
|
|
1035
|
-
|
|
1036
|
-
|
|
1152
|
+
}
|
|
1153
|
+
analyser.getByteFrequencyData(dataArray);
|
|
1154
|
+
let sum = 0;
|
|
1155
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
1156
|
+
sum += dataArray[i];
|
|
1157
|
+
}
|
|
1158
|
+
const avg = sum / dataArray.length;
|
|
1037
1159
|
const now = Date.now();
|
|
1038
|
-
if (avg >
|
|
1039
|
-
|
|
1040
|
-
if (!
|
|
1041
|
-
|
|
1160
|
+
if (avg > THRESHOLD) {
|
|
1161
|
+
lastSoundTime = now;
|
|
1162
|
+
if (!isSpeaking) {
|
|
1163
|
+
isSpeaking = true;
|
|
1164
|
+
console.log(`[VoiceDebug] Bot audio energy detected (speaking=true) — avg=${avg.toFixed(1)} — ${new Date().toISOString()}`);
|
|
1042
1165
|
this.ngZone.run(() => {
|
|
1043
1166
|
this.userSpeakingSubject.next(false);
|
|
1044
1167
|
this.speakingSubject.next(true);
|
|
1045
1168
|
});
|
|
1046
1169
|
}
|
|
1047
1170
|
}
|
|
1048
|
-
else if (
|
|
1049
|
-
|
|
1171
|
+
else if (isSpeaking && now - lastSoundTime > SILENCE_MS) {
|
|
1172
|
+
isSpeaking = false;
|
|
1173
|
+
console.log(`[VoiceDebug] Bot audio silence detected (speaking=false) — ${new Date().toISOString()}`);
|
|
1050
1174
|
this.ngZone.run(() => this.speakingSubject.next(false));
|
|
1051
1175
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1176
|
+
}, POLL_MS);
|
|
1177
|
+
}
|
|
1178
|
+
catch (err) {
|
|
1179
|
+
console.warn('DailyVoiceClient: failed to create remote audio monitor', err);
|
|
1055
1180
|
}
|
|
1056
|
-
catch (_a) { }
|
|
1057
1181
|
}
|
|
1058
1182
|
stopRemoteAudioMonitor() {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1183
|
+
if (this.remoteSpeakingPollId !== null) {
|
|
1184
|
+
clearInterval(this.remoteSpeakingPollId);
|
|
1185
|
+
this.remoteSpeakingPollId = null;
|
|
1186
|
+
}
|
|
1187
|
+
if (this.remoteAudioContext) {
|
|
1188
|
+
this.remoteAudioContext.close().catch(() => { });
|
|
1189
|
+
this.remoteAudioContext = null;
|
|
1063
1190
|
}
|
|
1064
|
-
(_a = this.remoteAudioContext) === null || _a === void 0 ? void 0 : _a.close().catch(() => { });
|
|
1065
|
-
this.remoteAudioContext = null;
|
|
1066
1191
|
}
|
|
1067
1192
|
stopRemoteAudio() {
|
|
1068
1193
|
if (this.remoteAudioElement) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1194
|
+
try {
|
|
1195
|
+
this.remoteAudioElement.pause();
|
|
1196
|
+
this.remoteAudioElement.srcObject = null;
|
|
1197
|
+
}
|
|
1198
|
+
catch (_) { }
|
|
1071
1199
|
this.remoteAudioElement = null;
|
|
1072
1200
|
}
|
|
1073
1201
|
}
|
|
1202
|
+
/** Set mic muted state. */
|
|
1074
1203
|
setMuted(muted) {
|
|
1075
1204
|
if (!this.callObject)
|
|
1076
1205
|
return;
|
|
1077
1206
|
this.callObject.setLocalAudio(!muted);
|
|
1078
1207
|
this.micMutedSubject.next(muted);
|
|
1079
|
-
console.log(`[VoiceDebug] Mic ${muted ? 'MUTED' : 'UNMUTED'}`);
|
|
1080
1208
|
}
|
|
1209
|
+
/** Disconnect and cleanup. */
|
|
1081
1210
|
disconnect() {
|
|
1082
1211
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1083
|
-
if (!this.callObject)
|
|
1084
|
-
|
|
1212
|
+
if (!this.callObject) {
|
|
1213
|
+
this.cleanup();
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1085
1216
|
try {
|
|
1086
1217
|
yield this.callObject.leave();
|
|
1087
1218
|
}
|
|
1088
|
-
catch (
|
|
1219
|
+
catch (e) {
|
|
1220
|
+
// ignore
|
|
1221
|
+
}
|
|
1089
1222
|
this.cleanup();
|
|
1090
1223
|
});
|
|
1091
1224
|
}
|
|
1092
1225
|
cleanup() {
|
|
1093
|
-
var _a, _b;
|
|
1094
1226
|
this.stopRemoteAudioMonitor();
|
|
1095
1227
|
this.stopRemoteAudio();
|
|
1096
|
-
(
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1228
|
+
if (this.callObject) {
|
|
1229
|
+
this.callObject.destroy().catch(() => { });
|
|
1230
|
+
this.callObject = null;
|
|
1231
|
+
}
|
|
1232
|
+
if (this.localStream) {
|
|
1233
|
+
this.localStream.getTracks().forEach((t) => t.stop());
|
|
1234
|
+
this.localStream = null;
|
|
1235
|
+
}
|
|
1100
1236
|
this.localSessionId = null;
|
|
1101
1237
|
this.speakingSubject.next(false);
|
|
1102
1238
|
this.userSpeakingSubject.next(false);
|
|
1103
1239
|
this.localStreamSubject.next(null);
|
|
1240
|
+
// Keep last micMuted state; will reset on next connect
|
|
1104
1241
|
}
|
|
1105
1242
|
}
|
|
1106
1243
|
DailyVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function DailyVoiceClientService_Factory() { return new DailyVoiceClientService(i0.ɵɵinject(i0.NgZone)); }, token: DailyVoiceClientService, providedIn: "root" });
|
|
@@ -1113,8 +1250,21 @@ DailyVoiceClientService.ctorParameters = () => [
|
|
|
1113
1250
|
{ type: NgZone }
|
|
1114
1251
|
];
|
|
1115
1252
|
|
|
1253
|
+
/**
|
|
1254
|
+
* Voice agent orchestrator. Coordinates WebSocket (signaling) and Daily.js (WebRTC audio).
|
|
1255
|
+
*
|
|
1256
|
+
* CRITICAL: This service must NEVER use Socket.IO or ngx-socket-io. Voice flow uses only:
|
|
1257
|
+
* - Native WebSocket (WebSocketVoiceClientService) for signaling (room_created, transcripts)
|
|
1258
|
+
* - Daily.js (DailyVoiceClientService) for WebRTC audio. Audio does NOT flow over WebSocket.
|
|
1259
|
+
*
|
|
1260
|
+
* - Maintains callState, statusText, duration, isMicMuted, isUserSpeaking, audioLevels
|
|
1261
|
+
* - Uses WebSocket for room_created and transcripts only (no audio)
|
|
1262
|
+
* - Uses Daily.js for all audio, mic, and real-time speaking detection
|
|
1263
|
+
*/
|
|
1116
1264
|
class VoiceAgentService {
|
|
1117
|
-
constructor(audioAnalyzer, wsClient, dailyClient, platformTokenRefresh,
|
|
1265
|
+
constructor(audioAnalyzer, wsClient, dailyClient, platformTokenRefresh,
|
|
1266
|
+
/** `Object` not `object` — ngc metadata collection rejects the `object` type in DI params. */
|
|
1267
|
+
platformId) {
|
|
1118
1268
|
this.audioAnalyzer = audioAnalyzer;
|
|
1119
1269
|
this.wsClient = wsClient;
|
|
1120
1270
|
this.dailyClient = dailyClient;
|
|
@@ -1123,7 +1273,7 @@ class VoiceAgentService {
|
|
|
1123
1273
|
this.callStateSubject = new BehaviorSubject('idle');
|
|
1124
1274
|
this.statusTextSubject = new BehaviorSubject('');
|
|
1125
1275
|
this.durationSubject = new BehaviorSubject('00:00');
|
|
1126
|
-
this.isMicMutedSubject = new BehaviorSubject(
|
|
1276
|
+
this.isMicMutedSubject = new BehaviorSubject(false);
|
|
1127
1277
|
this.isUserSpeakingSubject = new BehaviorSubject(false);
|
|
1128
1278
|
this.audioLevelsSubject = new BehaviorSubject([]);
|
|
1129
1279
|
this.userTranscriptSubject = new Subject();
|
|
@@ -1131,6 +1281,8 @@ class VoiceAgentService {
|
|
|
1131
1281
|
this.callStartTime = 0;
|
|
1132
1282
|
this.durationInterval = null;
|
|
1133
1283
|
this.subscriptions = new Subscription();
|
|
1284
|
+
/** Per-call only; cleared on disconnect / reset / new room so handlers do not stack. */
|
|
1285
|
+
this.callSubscriptions = new Subscription();
|
|
1134
1286
|
this.destroy$ = new Subject();
|
|
1135
1287
|
this.callState$ = this.callStateSubject.asObservable();
|
|
1136
1288
|
this.statusText$ = this.statusTextSubject.asObservable();
|
|
@@ -1140,118 +1292,163 @@ class VoiceAgentService {
|
|
|
1140
1292
|
this.audioLevels$ = this.audioLevelsSubject.asObservable();
|
|
1141
1293
|
this.userTranscript$ = this.userTranscriptSubject.asObservable();
|
|
1142
1294
|
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
1295
|
+
// Waveform visualization only - do NOT use for speaking state
|
|
1143
1296
|
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)));
|
|
1144
1306
|
}
|
|
1145
1307
|
ngOnDestroy() {
|
|
1146
1308
|
this.destroy$.next();
|
|
1147
1309
|
this.subscriptions.unsubscribe();
|
|
1148
1310
|
this.disconnect();
|
|
1149
1311
|
}
|
|
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
|
-
*/
|
|
1312
|
+
/** Reset to idle state (e.g. when modal opens so user can click Start Call). */
|
|
1154
1313
|
resetToIdle() {
|
|
1314
|
+
if (this.callStateSubject.value === 'idle')
|
|
1315
|
+
return;
|
|
1316
|
+
this.callSubscriptions.unsubscribe();
|
|
1317
|
+
this.callSubscriptions = new Subscription();
|
|
1155
1318
|
this.stopDurationTimer();
|
|
1156
1319
|
this.audioAnalyzer.stop();
|
|
1157
1320
|
this.wsClient.disconnect();
|
|
1321
|
+
// Fire-and-forget: Daily disconnect is async; connect() will await if needed
|
|
1158
1322
|
void this.dailyClient.disconnect();
|
|
1159
1323
|
this.callStateSubject.next('idle');
|
|
1160
1324
|
this.statusTextSubject.next('');
|
|
1161
|
-
this.durationSubject.next('
|
|
1162
|
-
this.isMicMutedSubject.next(true);
|
|
1163
|
-
this.isUserSpeakingSubject.next(false);
|
|
1164
|
-
this.audioLevelsSubject.next([]);
|
|
1325
|
+
this.durationSubject.next('0:00');
|
|
1165
1326
|
}
|
|
1166
|
-
connect(apiUrl, token, botId, conversationId, apiKey, eventToken, eventId, eventUrl, domainAuthority, usersApiUrl) {
|
|
1327
|
+
connect(apiUrl, token, botId, conversationId, apiKey, eventToken, eventId, eventUrl, domainAuthority, usersApiUrl, existingMicStream) {
|
|
1328
|
+
var _a;
|
|
1167
1329
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1168
|
-
if (this.callStateSubject.value !== 'idle')
|
|
1330
|
+
if (this.callStateSubject.value !== 'idle') {
|
|
1331
|
+
console.warn('Call already in progress');
|
|
1169
1332
|
return;
|
|
1333
|
+
}
|
|
1170
1334
|
try {
|
|
1171
1335
|
this.callStateSubject.next('connecting');
|
|
1172
1336
|
this.statusTextSubject.next('Connecting...');
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1337
|
+
const tokenPromise = usersApiUrl && isPlatformBrowser(this.platformId)
|
|
1338
|
+
? this.platformTokenRefresh
|
|
1339
|
+
.ensureValidAccessToken(token, usersApiUrl)
|
|
1340
|
+
.pipe(take(1))
|
|
1341
|
+
.toPromise()
|
|
1342
|
+
.then((ensured) => { var _a; return (_a = ensured === null || ensured === void 0 ? void 0 : ensured.accessToken) !== null && _a !== void 0 ? _a : token; })
|
|
1343
|
+
.catch((e) => {
|
|
1344
|
+
console.warn('[HiveGpt Voice] Token refresh before connect failed', e);
|
|
1345
|
+
return token;
|
|
1346
|
+
})
|
|
1347
|
+
: Promise.resolve(token);
|
|
1348
|
+
const prepPromise = Promise.resolve().then(() => {
|
|
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
|
+
]);
|
|
1372
|
+
const headers = {
|
|
1373
|
+
'Content-Type': 'application/json',
|
|
1374
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1375
|
+
'x-api-key': apiKey,
|
|
1376
|
+
'hive-bot-id': botId,
|
|
1377
|
+
'domain-authority': domainAuthority,
|
|
1378
|
+
eventUrl,
|
|
1379
|
+
eventId,
|
|
1380
|
+
eventToken,
|
|
1381
|
+
'ngrok-skip-browser-warning': 'true',
|
|
1382
|
+
};
|
|
1383
|
+
// POST to get ws_url for signaling
|
|
1190
1384
|
const res = yield fetch(postUrl, {
|
|
1191
1385
|
method: 'POST',
|
|
1192
|
-
headers
|
|
1193
|
-
|
|
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
|
-
},
|
|
1202
|
-
body: JSON.stringify({
|
|
1203
|
-
bot_id: botId,
|
|
1204
|
-
conversation_id: conversationId,
|
|
1205
|
-
voice: 'alloy',
|
|
1206
|
-
}),
|
|
1386
|
+
headers,
|
|
1387
|
+
body,
|
|
1207
1388
|
});
|
|
1389
|
+
if (!res.ok) {
|
|
1390
|
+
throw new Error(`HTTP ${res.status}`);
|
|
1391
|
+
}
|
|
1208
1392
|
const json = yield res.json();
|
|
1209
1393
|
const wsUrl = json === null || json === void 0 ? void 0 : json.rn_ws_url;
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1394
|
+
if (!wsUrl || typeof wsUrl !== 'string') {
|
|
1395
|
+
throw new Error('No ws_url in response');
|
|
1396
|
+
}
|
|
1397
|
+
// Subscribe before connect so the first room_created is never missed.
|
|
1398
|
+
// Await until Daily join completes — callers must not treat WS "open" as call ready.
|
|
1399
|
+
let roomCreatedSub;
|
|
1400
|
+
const roomJoined = new Promise((resolve, reject) => {
|
|
1401
|
+
roomCreatedSub = this.wsClient.roomCreated$
|
|
1402
|
+
.pipe(take(1), takeUntil(this.destroy$))
|
|
1403
|
+
.subscribe((roomUrl) => __awaiter(this, void 0, void 0, function* () {
|
|
1404
|
+
try {
|
|
1405
|
+
yield this.onRoomCreated(roomUrl, micStream !== null && micStream !== void 0 ? micStream : undefined);
|
|
1406
|
+
resolve();
|
|
1407
|
+
}
|
|
1408
|
+
catch (err) {
|
|
1409
|
+
console.error('Daily join failed:', err);
|
|
1410
|
+
reject(err);
|
|
1411
|
+
}
|
|
1412
|
+
}), (err) => reject(err));
|
|
1413
|
+
});
|
|
1414
|
+
try {
|
|
1415
|
+
yield this.wsClient.connect(wsUrl);
|
|
1416
|
+
yield roomJoined;
|
|
1417
|
+
}
|
|
1418
|
+
catch (e) {
|
|
1419
|
+
roomCreatedSub === null || roomCreatedSub === void 0 ? void 0 : roomCreatedSub.unsubscribe();
|
|
1420
|
+
throw e;
|
|
1421
|
+
}
|
|
1218
1422
|
}
|
|
1219
|
-
catch (
|
|
1423
|
+
catch (error) {
|
|
1424
|
+
console.error('Error connecting voice agent:', error);
|
|
1220
1425
|
this.callStateSubject.next('ended');
|
|
1426
|
+
yield this.disconnect();
|
|
1427
|
+
this.statusTextSubject.next('Connection failed');
|
|
1428
|
+
throw error;
|
|
1221
1429
|
}
|
|
1222
1430
|
});
|
|
1223
1431
|
}
|
|
1224
|
-
onRoomCreated(roomUrl) {
|
|
1432
|
+
onRoomCreated(roomUrl, micStream) {
|
|
1225
1433
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1226
|
-
yield this.dailyClient.connect(roomUrl);
|
|
1227
|
-
|
|
1228
|
-
this.
|
|
1229
|
-
|
|
1230
|
-
this.
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
console.log('[VoiceFlow] First bot response → enabling mic');
|
|
1238
|
-
this.dailyClient.setMuted(false);
|
|
1239
|
-
this.statusTextSubject.next('You can speak now');
|
|
1240
|
-
});
|
|
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
|
|
1250
|
-
this.subscriptions.add(combineLatest([
|
|
1434
|
+
yield this.dailyClient.connect(roomUrl, undefined, micStream);
|
|
1435
|
+
this.callSubscriptions.unsubscribe();
|
|
1436
|
+
this.callSubscriptions = new Subscription();
|
|
1437
|
+
// Waveform: use local mic stream from Daily client
|
|
1438
|
+
this.callSubscriptions.add(this.dailyClient.localStream$
|
|
1439
|
+
.pipe(filter((s) => s != null), take(1))
|
|
1440
|
+
.subscribe((stream) => {
|
|
1441
|
+
this.audioAnalyzer.start(stream);
|
|
1442
|
+
}));
|
|
1443
|
+
this.callSubscriptions.add(this.dailyClient.userSpeaking$.subscribe((s) => this.isUserSpeakingSubject.next(s)));
|
|
1444
|
+
this.callSubscriptions.add(combineLatest([
|
|
1251
1445
|
this.dailyClient.speaking$,
|
|
1252
1446
|
this.dailyClient.userSpeaking$,
|
|
1253
1447
|
]).subscribe(([bot, user]) => {
|
|
1254
1448
|
const current = this.callStateSubject.value;
|
|
1449
|
+
if (current === 'connecting' && !bot) {
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1255
1452
|
if (current === 'connecting' && bot) {
|
|
1256
1453
|
this.callStartTime = Date.now();
|
|
1257
1454
|
this.startDurationTimer();
|
|
@@ -1265,15 +1462,21 @@ class VoiceAgentService {
|
|
|
1265
1462
|
this.callStateSubject.next('talking');
|
|
1266
1463
|
}
|
|
1267
1464
|
else {
|
|
1268
|
-
|
|
1465
|
+
// Between bot turns: stay on listening to avoid flicker via 'connected'
|
|
1466
|
+
this.callStateSubject.next('listening');
|
|
1269
1467
|
}
|
|
1270
1468
|
}));
|
|
1469
|
+
this.callSubscriptions.add(this.dailyClient.micMuted$.subscribe((muted) => this.isMicMutedSubject.next(muted)));
|
|
1470
|
+
this.statusTextSubject.next('Connecting...');
|
|
1271
1471
|
});
|
|
1272
1472
|
}
|
|
1273
1473
|
disconnect() {
|
|
1274
1474
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1475
|
+
this.callSubscriptions.unsubscribe();
|
|
1476
|
+
this.callSubscriptions = new Subscription();
|
|
1275
1477
|
this.stopDurationTimer();
|
|
1276
1478
|
this.audioAnalyzer.stop();
|
|
1479
|
+
// Daily first, then WebSocket
|
|
1277
1480
|
yield this.dailyClient.disconnect();
|
|
1278
1481
|
this.wsClient.disconnect();
|
|
1279
1482
|
this.callStateSubject.next('ended');
|
|
@@ -1285,16 +1488,22 @@ class VoiceAgentService {
|
|
|
1285
1488
|
this.dailyClient.setMuted(!current);
|
|
1286
1489
|
}
|
|
1287
1490
|
startDurationTimer() {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1491
|
+
const updateDuration = () => {
|
|
1492
|
+
if (this.callStartTime > 0) {
|
|
1493
|
+
const elapsed = Math.floor((Date.now() - this.callStartTime) / 1000);
|
|
1494
|
+
const minutes = Math.floor(elapsed / 60);
|
|
1495
|
+
const seconds = elapsed % 60;
|
|
1496
|
+
this.durationSubject.next(`${minutes}:${String(seconds).padStart(2, '0')}`);
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
updateDuration();
|
|
1500
|
+
this.durationInterval = setInterval(updateDuration, 1000);
|
|
1294
1501
|
}
|
|
1295
1502
|
stopDurationTimer() {
|
|
1296
|
-
if (this.durationInterval)
|
|
1503
|
+
if (this.durationInterval) {
|
|
1297
1504
|
clearInterval(this.durationInterval);
|
|
1505
|
+
this.durationInterval = null;
|
|
1506
|
+
}
|
|
1298
1507
|
}
|
|
1299
1508
|
}
|
|
1300
1509
|
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" });
|
|
@@ -1315,10 +1524,11 @@ const VOICE_MODAL_CONFIG = new InjectionToken('VOICE_MODAL_CONFIG');
|
|
|
1315
1524
|
const VOICE_MODAL_CLOSE_CALLBACK = new InjectionToken('VOICE_MODAL_CLOSE_CALLBACK');
|
|
1316
1525
|
|
|
1317
1526
|
class VoiceAgentModalComponent {
|
|
1318
|
-
constructor(voiceAgentService, audioAnalyzer, injector) {
|
|
1527
|
+
constructor(voiceAgentService, audioAnalyzer, injector, platformId) {
|
|
1319
1528
|
this.voiceAgentService = voiceAgentService;
|
|
1320
1529
|
this.audioAnalyzer = audioAnalyzer;
|
|
1321
1530
|
this.injector = injector;
|
|
1531
|
+
this.platformId = platformId;
|
|
1322
1532
|
this.close = new EventEmitter();
|
|
1323
1533
|
this.apiKey = '';
|
|
1324
1534
|
this.eventToken = '';
|
|
@@ -1330,6 +1540,8 @@ class VoiceAgentModalComponent {
|
|
|
1330
1540
|
this.usersApiUrl = '';
|
|
1331
1541
|
this.injectedConfig = null;
|
|
1332
1542
|
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;
|
|
1333
1545
|
/** Hardcoded voice agent avatar (Nia). */
|
|
1334
1546
|
this.displayAvatarUrl = 'https://www.jotform.com/uploads/mehmetkarakasli/form_files/1564593667676a8e85f23758.86945537_icon.png';
|
|
1335
1547
|
this.callState = 'idle';
|
|
@@ -1345,58 +1557,83 @@ class VoiceAgentModalComponent {
|
|
|
1345
1557
|
this.isConnecting = false;
|
|
1346
1558
|
}
|
|
1347
1559
|
ngOnInit() {
|
|
1560
|
+
void this.bootstrap();
|
|
1561
|
+
}
|
|
1562
|
+
bootstrap() {
|
|
1348
1563
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
}
|
|
1367
|
-
// Subscribe to observables
|
|
1368
|
-
this.subscriptions.push(this.voiceAgentService.callState$.subscribe(state => {
|
|
1369
|
-
this.callState = state;
|
|
1370
|
-
this.isSpeaking = state === 'talking';
|
|
1371
|
-
if (state === 'listening' || state === 'talking') {
|
|
1372
|
-
this.hasLeftConnectedOnce = true;
|
|
1373
|
-
}
|
|
1374
|
-
if (state === 'idle' || state === 'ended') {
|
|
1375
|
-
this.hasLeftConnectedOnce = false;
|
|
1564
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1565
|
+
this.injectedConfig = this.injector.get(VOICE_MODAL_CONFIG, null);
|
|
1566
|
+
this.onCloseCallback = this.injector.get(VOICE_MODAL_CLOSE_CALLBACK, null);
|
|
1567
|
+
if (this.injectedConfig) {
|
|
1568
|
+
this.apiUrl = this.injectedConfig.apiUrl;
|
|
1569
|
+
this.token = this.injectedConfig.token;
|
|
1570
|
+
this.botId = this.injectedConfig.botId;
|
|
1571
|
+
this.conversationId = this.injectedConfig.conversationId;
|
|
1572
|
+
this.apiKey = (_a = this.injectedConfig.apiKey) !== null && _a !== void 0 ? _a : '';
|
|
1573
|
+
this.eventToken = (_b = this.injectedConfig.eventToken) !== null && _b !== void 0 ? _b : '';
|
|
1574
|
+
this.eventId = (_c = this.injectedConfig.eventId) !== null && _c !== void 0 ? _c : '';
|
|
1575
|
+
this.eventUrl = (_d = this.injectedConfig.eventUrl) !== null && _d !== void 0 ? _d : '';
|
|
1576
|
+
this.domainAuthority = (_e = this.injectedConfig.domainAuthority) !== null && _e !== void 0 ? _e : 'prod-lite';
|
|
1577
|
+
this.agentName = (_f = this.injectedConfig.agentName) !== null && _f !== void 0 ? _f : this.agentName;
|
|
1578
|
+
this.agentRole = (_g = this.injectedConfig.agentRole) !== null && _g !== void 0 ? _g : this.agentRole;
|
|
1579
|
+
this.agentAvatar = this.injectedConfig.agentAvatar;
|
|
1580
|
+
this.usersApiUrl = (_h = this.injectedConfig.usersApiUrl) !== null && _h !== void 0 ? _h : this.usersApiUrl;
|
|
1376
1581
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
this.
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
this.
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1582
|
+
// Subscribe to observables
|
|
1583
|
+
this.subscriptions.push(this.voiceAgentService.callState$.subscribe(state => {
|
|
1584
|
+
this.callState = state;
|
|
1585
|
+
this.isSpeaking = state === 'talking';
|
|
1586
|
+
if (state === 'listening' || state === 'talking') {
|
|
1587
|
+
this.hasLeftConnectedOnce = true;
|
|
1588
|
+
}
|
|
1589
|
+
if (state === 'idle' || state === 'ended') {
|
|
1590
|
+
this.hasLeftConnectedOnce = false;
|
|
1591
|
+
}
|
|
1592
|
+
}));
|
|
1593
|
+
this.subscriptions.push(this.voiceAgentService.statusText$.subscribe(text => {
|
|
1594
|
+
this.statusText = text;
|
|
1595
|
+
}));
|
|
1596
|
+
this.subscriptions.push(this.voiceAgentService.duration$.subscribe(duration => {
|
|
1597
|
+
this.duration = duration;
|
|
1598
|
+
}));
|
|
1599
|
+
this.subscriptions.push(this.voiceAgentService.isMicMuted$.subscribe(muted => {
|
|
1600
|
+
this.isMicMuted = muted;
|
|
1601
|
+
}));
|
|
1602
|
+
this.subscriptions.push(this.voiceAgentService.isUserSpeaking$.subscribe(speaking => {
|
|
1603
|
+
this.isUserSpeaking = speaking;
|
|
1604
|
+
}));
|
|
1605
|
+
this.subscriptions.push(this.voiceAgentService.audioLevels$.subscribe(levels => {
|
|
1606
|
+
this.audioLevels = levels;
|
|
1607
|
+
}));
|
|
1608
|
+
this.voiceAgentService.resetToIdle();
|
|
1609
|
+
yield this.startCall();
|
|
1610
|
+
});
|
|
1396
1611
|
}
|
|
1397
1612
|
ngOnDestroy() {
|
|
1398
|
-
this.subscriptions.forEach(sub => sub.unsubscribe());
|
|
1399
|
-
this.disconnect()
|
|
1613
|
+
this.subscriptions.forEach((sub) => sub.unsubscribe());
|
|
1614
|
+
void this.disconnect().finally(() => {
|
|
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
|
+
});
|
|
1400
1637
|
}
|
|
1401
1638
|
startCall() {
|
|
1402
1639
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -1404,7 +1641,8 @@ class VoiceAgentModalComponent {
|
|
|
1404
1641
|
return;
|
|
1405
1642
|
this.isConnecting = true;
|
|
1406
1643
|
try {
|
|
1407
|
-
|
|
1644
|
+
const mic = yield this.ensureMicForCall();
|
|
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);
|
|
1408
1646
|
}
|
|
1409
1647
|
catch (error) {
|
|
1410
1648
|
console.error('Failed to connect voice agent:', error);
|
|
@@ -1443,7 +1681,7 @@ class VoiceAgentModalComponent {
|
|
|
1443
1681
|
/** Call Again: reset to idle then start a new call. */
|
|
1444
1682
|
callAgain() {
|
|
1445
1683
|
this.voiceAgentService.resetToIdle();
|
|
1446
|
-
this.startCall();
|
|
1684
|
+
void this.startCall();
|
|
1447
1685
|
}
|
|
1448
1686
|
/** Back to Chat: close modal and disconnect. */
|
|
1449
1687
|
backToChat() {
|
|
@@ -1470,7 +1708,8 @@ VoiceAgentModalComponent.decorators = [
|
|
|
1470
1708
|
VoiceAgentModalComponent.ctorParameters = () => [
|
|
1471
1709
|
{ type: VoiceAgentService },
|
|
1472
1710
|
{ type: AudioAnalyzerService },
|
|
1473
|
-
{ type: Injector }
|
|
1711
|
+
{ type: Injector },
|
|
1712
|
+
{ type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }
|
|
1474
1713
|
];
|
|
1475
1714
|
VoiceAgentModalComponent.propDecorators = {
|
|
1476
1715
|
close: [{ type: Output }],
|
|
@@ -4843,7 +5082,7 @@ class ChatDrawerComponent {
|
|
|
4843
5082
|
}); // returns current time in 'shortTime' format
|
|
4844
5083
|
}
|
|
4845
5084
|
openVoiceModal() {
|
|
4846
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m
|
|
5085
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
4847
5086
|
const conversationId = (_b = (_a = this.conversationKey) !== null && _a !== void 0 ? _a : this.conversationService.getKey(this.botId, false, this.eventId)) !== null && _b !== void 0 ? _b : '';
|
|
4848
5087
|
this.voiceModalConversationId = conversationId;
|
|
4849
5088
|
this.setupVoiceTranscripts();
|
|
@@ -4860,13 +5099,13 @@ class ChatDrawerComponent {
|
|
|
4860
5099
|
conversationId,
|
|
4861
5100
|
apiKey: (_g = this.apiKey) !== null && _g !== void 0 ? _g : '',
|
|
4862
5101
|
eventToken: (_h = this.eventToken) !== null && _h !== void 0 ? _h : '',
|
|
4863
|
-
eventId:
|
|
4864
|
-
eventUrl: (
|
|
4865
|
-
domainAuthority: (
|
|
5102
|
+
eventId: this.eventId,
|
|
5103
|
+
eventUrl: (_j = this.eventUrl) !== null && _j !== void 0 ? _j : '',
|
|
5104
|
+
domainAuthority: (_k = this.domainAuthorityValue) !== null && _k !== void 0 ? _k : 'prod-lite',
|
|
4866
5105
|
agentName: this.botName || 'AI Assistant',
|
|
4867
5106
|
agentRole: this.botSkills || 'AI Agent Specialist',
|
|
4868
5107
|
agentAvatar: this.botIcon,
|
|
4869
|
-
usersApiUrl: (
|
|
5108
|
+
usersApiUrl: (_m = (_l = this.environment) === null || _l === void 0 ? void 0 : _l.USERS_API) !== null && _m !== void 0 ? _m : '',
|
|
4870
5109
|
};
|
|
4871
5110
|
const closeCallback = () => {
|
|
4872
5111
|
if (this.voiceModalOverlayRef) {
|