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