@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.
Files changed (21) hide show
  1. package/bundles/hivegpt-hiveai-angular.umd.js +542 -278
  2. package/bundles/hivegpt-hiveai-angular.umd.js.map +1 -1
  3. package/bundles/hivegpt-hiveai-angular.umd.min.js +1 -1
  4. package/bundles/hivegpt-hiveai-angular.umd.min.js.map +1 -1
  5. package/esm2015/lib/components/chat-drawer/chat-drawer.component.js +6 -6
  6. package/esm2015/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.js +85 -54
  7. package/esm2015/lib/components/voice-agent/services/daily-voice-client.service.js +153 -63
  8. package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +160 -88
  9. package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +81 -34
  10. package/fesm2015/hivegpt-hiveai-angular.js +478 -239
  11. package/fesm2015/hivegpt-hiveai-angular.js.map +1 -1
  12. package/hivegpt-hiveai-angular.metadata.json +1 -1
  13. package/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts +7 -1
  14. package/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts.map +1 -1
  15. package/lib/components/voice-agent/services/daily-voice-client.service.d.ts +37 -3
  16. package/lib/components/voice-agent/services/daily-voice-client.service.d.ts.map +1 -1
  17. package/lib/components/voice-agent/services/voice-agent.service.d.ts +19 -6
  18. package/lib/components/voice-agent/services/voice-agent.service.d.ts.map +1 -1
  19. package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts +5 -12
  20. package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts.map +1 -1
  21. 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
- /** Connect to signaling WebSocket. No audio over this connection. */
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
- try {
840
- this.ws = new WebSocket(wsUrl);
841
- this.ws.onmessage = (event) => {
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
- const msg = JSON.parse(event.data);
845
- if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'room_created') {
846
- const roomUrl = ((_a = msg.room_url) !== null && _a !== void 0 ? _a : msg.roomUrl);
847
- if (typeof roomUrl === 'string') {
848
- this.roomCreatedSubject.next(roomUrl);
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
- // Ignore non-JSON or unknown messages
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
- catch (err) {
873
- console.error('WebSocketVoiceClient: connect failed', err);
874
- this.ws = null;
875
- throw err;
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
- this.remoteSpeakingRAF = null;
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(true); // 🔴 default muted
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
- connect(roomUrl, token) {
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
- // 🎤 Get mic (kept for waveform)
923
- const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
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
- // 🔴 Ensure mic is OFF before join
936
- callObject.setLocalAudio(false);
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] Joined room — ${new Date().toISOString()}`);
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
- // 🔴 Force sync again (Daily sometimes overrides)
949
- callObject.setLocalAudio(false);
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
- this.userSpeakingSubject.next(peerId === this.localSessionId);
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, _e;
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
- const track = (_c = event.track) !== null && _c !== void 0 ? _c : (_e = (_d = p === null || p === void 0 ? void 0 : p.tracks) === null || _d === void 0 ? void 0 : _d.audio) === null || _e === void 0 ? void 0 : _e.track;
977
- if (track) {
978
- console.log('[VoiceDebug] Remote audio track received');
979
- this.playRemoteTrack(track);
980
- this.monitorRemoteAudio(track);
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', (e) => {
1000
- console.error('Daily error:', e);
1001
- this.cleanup();
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.play().catch(() => {
1013
- console.warn('Autoplay blocked');
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('Audio playback error', err);
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 data = new Uint8Array(analyser.frequencyBinCount);
1030
- let speaking = false;
1031
- let lastSound = 0;
1032
- const loop = () => {
1033
- if (!this.remoteAudioContext)
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
- analyser.getByteFrequencyData(data);
1036
- const avg = data.reduce((a, b) => a + b, 0) / data.length;
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 > 5) {
1039
- lastSound = now;
1040
- if (!speaking) {
1041
- speaking = true;
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 (speaking && now - lastSound > 1500) {
1049
- speaking = false;
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
- this.remoteSpeakingRAF = requestAnimationFrame(loop);
1053
- };
1054
- this.remoteSpeakingRAF = requestAnimationFrame(loop);
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
- var _a;
1060
- if (this.remoteSpeakingRAF) {
1061
- cancelAnimationFrame(this.remoteSpeakingRAF);
1062
- this.remoteSpeakingRAF = null;
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
- this.remoteAudioElement.pause();
1070
- this.remoteAudioElement.srcObject = null;
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
- return this.cleanup();
1212
+ if (!this.callObject) {
1213
+ this.cleanup();
1214
+ return;
1215
+ }
1085
1216
  try {
1086
1217
  yield this.callObject.leave();
1087
1218
  }
1088
- catch (_a) { }
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
- (_a = this.callObject) === null || _a === void 0 ? void 0 : _a.destroy().catch(() => { });
1097
- this.callObject = null;
1098
- (_b = this.localStream) === null || _b === void 0 ? void 0 : _b.getTracks().forEach((t) => t.stop());
1099
- this.localStream = null;
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, platformId) {
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(true);
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('00:00');
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
- let accessToken = token;
1174
- if (usersApiUrl && isPlatformBrowser(this.platformId)) {
1175
- try {
1176
- const ensured = yield this.platformTokenRefresh
1177
- .ensureValidAccessToken(token, usersApiUrl)
1178
- .pipe(take(1))
1179
- .toPromise();
1180
- if (ensured === null || ensured === void 0 ? void 0 : ensured.accessToken) {
1181
- accessToken = ensured.accessToken;
1182
- }
1183
- }
1184
- catch (_a) { }
1185
- }
1186
- const base = (apiUrl || '').replace(/\/+$/, '');
1187
- const postUrl = `${base}/ai/ask-voice`;
1188
- // Same as chat-drawer `/ai/ask` headers: use `eventId` (camelCase), value from host `this.eventId`.
1189
- const eventIdHeader = (eventId && String(eventId).trim()) || '';
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
- 'Content-Type': 'application/json',
1194
- Authorization: `Bearer ${accessToken}`,
1195
- 'domain-authority': domainAuthority,
1196
- eventtoken: eventToken,
1197
- eventurl: eventUrl,
1198
- 'hive-bot-id': botId,
1199
- 'x-api-key': apiKey,
1200
- eventId: eventIdHeader,
1201
- },
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
- this.wsClient.roomCreated$
1211
- .pipe(take(1), takeUntil(this.destroy$))
1212
- .subscribe((roomUrl) => __awaiter(this, void 0, void 0, function* () {
1213
- yield this.onRoomCreated(roomUrl);
1214
- }));
1215
- this.subscriptions.add(this.wsClient.userTranscript$.subscribe((t) => this.userTranscriptSubject.next(t)));
1216
- this.subscriptions.add(this.wsClient.botTranscript$.subscribe((t) => this.botTranscriptSubject.next(t)));
1217
- this.wsClient.connect(wsUrl);
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 (e) {
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
- // 🔴 Start MUTED
1228
- this.dailyClient.setMuted(true);
1229
- this.isMicMutedSubject.next(true);
1230
- this.statusTextSubject.next('Listening to agent...');
1231
- // Enable mic on FIRST bot speech
1232
- let handled = false;
1233
- this.dailyClient.speaking$.pipe(filter(Boolean), take(1)).subscribe(() => {
1234
- if (handled)
1235
- return;
1236
- handled = true;
1237
- console.log('[VoiceFlow] First bot response → enabling mic');
1238
- this.dailyClient.setMuted(false);
1239
- this.statusTextSubject.next('You can speak now');
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
- this.callStateSubject.next('connected');
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
- this.durationInterval = setInterval(() => {
1289
- const elapsed = Math.floor((Date.now() - this.callStartTime) / 1000);
1290
- const m = Math.floor(elapsed / 60);
1291
- const s = elapsed % 60;
1292
- this.durationSubject.next(`${m}:${String(s).padStart(2, '0')}`);
1293
- }, 1000);
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
- // When opened via Overlay, config is provided by injection
1350
- this.injectedConfig = this.injector.get(VOICE_MODAL_CONFIG, null);
1351
- this.onCloseCallback = this.injector.get(VOICE_MODAL_CLOSE_CALLBACK, null);
1352
- if (this.injectedConfig) {
1353
- this.apiUrl = this.injectedConfig.apiUrl;
1354
- this.token = this.injectedConfig.token;
1355
- this.botId = this.injectedConfig.botId;
1356
- this.conversationId = this.injectedConfig.conversationId;
1357
- this.apiKey = (_a = this.injectedConfig.apiKey) !== null && _a !== void 0 ? _a : '';
1358
- this.eventToken = (_b = this.injectedConfig.eventToken) !== null && _b !== void 0 ? _b : '';
1359
- this.eventId = (_c = this.injectedConfig.eventId) !== null && _c !== void 0 ? _c : '';
1360
- this.eventUrl = (_d = this.injectedConfig.eventUrl) !== null && _d !== void 0 ? _d : '';
1361
- this.domainAuthority = (_e = this.injectedConfig.domainAuthority) !== null && _e !== void 0 ? _e : 'prod-lite';
1362
- this.agentName = (_f = this.injectedConfig.agentName) !== null && _f !== void 0 ? _f : this.agentName;
1363
- this.agentRole = (_g = this.injectedConfig.agentRole) !== null && _g !== void 0 ? _g : this.agentRole;
1364
- this.agentAvatar = this.injectedConfig.agentAvatar;
1365
- this.usersApiUrl = (_h = this.injectedConfig.usersApiUrl) !== null && _h !== void 0 ? _h : this.usersApiUrl;
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
- this.subscriptions.push(this.voiceAgentService.statusText$.subscribe(text => {
1379
- this.statusText = text;
1380
- }));
1381
- this.subscriptions.push(this.voiceAgentService.duration$.subscribe(duration => {
1382
- this.duration = duration;
1383
- }));
1384
- this.subscriptions.push(this.voiceAgentService.isMicMuted$.subscribe(muted => {
1385
- this.isMicMuted = muted;
1386
- }));
1387
- this.subscriptions.push(this.voiceAgentService.isUserSpeaking$.subscribe(speaking => {
1388
- this.isUserSpeaking = speaking;
1389
- }));
1390
- this.subscriptions.push(this.voiceAgentService.audioLevels$.subscribe(levels => {
1391
- this.audioLevels = levels;
1392
- }));
1393
- // Modal opens in idle state, then immediately starts connecting.
1394
- this.voiceAgentService.resetToIdle();
1395
- void this.startCall();
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
- 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);
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, _o;
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: (_j = this.eventId) !== null && _j !== void 0 ? _j : '',
4864
- eventUrl: (_k = this.eventUrl) !== null && _k !== void 0 ? _k : '',
4865
- domainAuthority: (_l = this.domainAuthorityValue) !== null && _l !== void 0 ? _l : 'prod-lite',
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: (_o = (_m = this.environment) === null || _m === void 0 ? void 0 : _m.USERS_API) !== null && _o !== void 0 ? _o : '',
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) {