@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.
Files changed (21) hide show
  1. package/bundles/hivegpt-hiveai-angular.umd.js +526 -271
  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 +149 -85
  9. package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +79 -34
  10. package/fesm2015/hivegpt-hiveai-angular.js +465 -236
  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,98 @@ 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
+ 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
- this.remoteSpeakingRAF = null;
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(true); // 🔴 default muted
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
- connect(roomUrl, token) {
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
- // 🎤 Get mic (kept for waveform)
923
- const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
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
- // 🔴 Ensure mic is OFF before join
936
- callObject.setLocalAudio(false);
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] Joined room — ${new Date().toISOString()}`);
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
- // 🔴 Force sync again (Daily sometimes overrides)
949
- callObject.setLocalAudio(false);
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
- this.userSpeakingSubject.next(peerId === this.localSessionId);
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, _e;
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
- 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);
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', (e) => {
1000
- console.error('Daily error:', e);
1001
- this.cleanup();
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.play().catch(() => {
1013
- console.warn('Autoplay blocked');
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('Audio playback error', err);
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 data = new Uint8Array(analyser.frequencyBinCount);
1030
- let speaking = false;
1031
- let lastSound = 0;
1032
- const loop = () => {
1033
- if (!this.remoteAudioContext)
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
- analyser.getByteFrequencyData(data);
1036
- const avg = data.reduce((a, b) => a + b, 0) / data.length;
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 > 5) {
1039
- lastSound = now;
1040
- if (!speaking) {
1041
- speaking = true;
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 (speaking && now - lastSound > 1500) {
1049
- speaking = false;
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
- this.remoteSpeakingRAF = requestAnimationFrame(loop);
1053
- };
1054
- this.remoteSpeakingRAF = requestAnimationFrame(loop);
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
- var _a;
1060
- if (this.remoteSpeakingRAF) {
1061
- cancelAnimationFrame(this.remoteSpeakingRAF);
1062
- this.remoteSpeakingRAF = null;
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
- this.remoteAudioElement.pause();
1070
- this.remoteAudioElement.srcObject = null;
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
- return this.cleanup();
1210
+ if (!this.callObject) {
1211
+ this.cleanup();
1212
+ return;
1213
+ }
1085
1214
  try {
1086
1215
  yield this.callObject.leave();
1087
1216
  }
1088
- catch (_a) { }
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
- (_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;
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, platformId) {
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(true);
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('00:00');
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 to agent...');
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 = `https://1356-103-210-33-236.ngrok-free.app/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()) || '';
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
- '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
- }),
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
- yield this.onRoomCreated(roomUrl);
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
- 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);
1410
+ // Connect signaling WebSocket (no audio over WS)
1411
+ yield this.wsClient.connect(wsUrl);
1218
1412
  }
1219
- catch (e) {
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
- // 🔴 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([
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
- this.callStateSubject.next('connected');
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
- 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);
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
- // 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;
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
- 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();
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
- 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);
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, _o;
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: (_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',
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: (_o = (_m = this.environment) === null || _m === void 0 ? void 0 : _m.USERS_API) !== null && _o !== void 0 ? _o : '',
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) {