@hivegpt/hiveai-angular 0.0.568 → 0.0.570

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