@hivegpt/hiveai-angular 0.0.424 → 0.0.427

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 (25) hide show
  1. package/bundles/hivegpt-hiveai-angular.umd.js +474 -158
  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/hivegpt-hiveai-angular.js +6 -4
  6. package/esm2015/lib/components/voice-agent/services/audio-analyzer.service.js +5 -1
  7. package/esm2015/lib/components/voice-agent/services/daily-voice-client.service.js +181 -0
  8. package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +128 -140
  9. package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +93 -0
  10. package/esm2015/lib/components/voice-agent/voice-agent.module.js +6 -2
  11. package/fesm2015/hivegpt-hiveai-angular.js +399 -143
  12. package/fesm2015/hivegpt-hiveai-angular.js.map +1 -1
  13. package/hivegpt-hiveai-angular.d.ts +5 -3
  14. package/hivegpt-hiveai-angular.d.ts.map +1 -1
  15. package/hivegpt-hiveai-angular.metadata.json +1 -1
  16. package/lib/components/voice-agent/services/audio-analyzer.service.d.ts +4 -0
  17. package/lib/components/voice-agent/services/audio-analyzer.service.d.ts.map +1 -1
  18. package/lib/components/voice-agent/services/daily-voice-client.service.d.ts +48 -0
  19. package/lib/components/voice-agent/services/daily-voice-client.service.d.ts.map +1 -0
  20. package/lib/components/voice-agent/services/voice-agent.service.d.ts +19 -8
  21. package/lib/components/voice-agent/services/voice-agent.service.d.ts.map +1 -1
  22. package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts +47 -0
  23. package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts.map +1 -0
  24. package/lib/components/voice-agent/voice-agent.module.d.ts.map +1 -1
  25. package/package.json +2 -1
@@ -3,17 +3,16 @@ import { ComponentPortal } from '@angular/cdk/portal';
3
3
  import * as i1 from '@angular/common/http';
4
4
  import { HttpHeaders, HttpClient } from '@angular/common/http';
5
5
  import * as i0 from '@angular/core';
6
- import { Injectable, InjectionToken, EventEmitter, Component, Injector, Output, Input, ElementRef, ChangeDetectionStrategy, ChangeDetectorRef, Renderer2, ViewContainerRef, ViewChild, ViewChildren, NgModule, Pipe, Inject } from '@angular/core';
6
+ import { Injectable, NgZone, InjectionToken, EventEmitter, Component, Injector, Output, Input, ElementRef, ChangeDetectionStrategy, ChangeDetectorRef, Renderer2, ViewContainerRef, ViewChild, ViewChildren, NgModule, Pipe, Inject } from '@angular/core';
7
7
  import { DomSanitizer } from '@angular/platform-browser';
8
- import { Subject, BehaviorSubject, of } from 'rxjs';
9
- import { map, switchMap, catchError } from 'rxjs/operators';
8
+ import { Subject, BehaviorSubject, Subscription, combineLatest, of } from 'rxjs';
9
+ import { map, take, takeUntil, filter, switchMap, catchError } from 'rxjs/operators';
10
10
  import { Socket } from 'ngx-socket-io';
11
11
  import { Validators, FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
12
12
  import * as SpeechSDK from 'microsoft-cognitiveservices-speech-sdk';
13
13
  import * as marked from 'marked';
14
14
  import { __awaiter } from 'tslib';
15
- import { PipecatClient, RTVIEvent } from '@pipecat-ai/client-js';
16
- import { WebSocketTransport } from '@pipecat-ai/websocket-transport';
15
+ import Daily from '@daily-co/daily-js';
17
16
  import { CommonModule, DOCUMENT } from '@angular/common';
18
17
  import { MatIconModule } from '@angular/material/icon';
19
18
  import { MatSidenavModule } from '@angular/material/sidenav';
@@ -501,6 +500,10 @@ BotsService.ctorParameters = () => [
501
500
  { type: HttpClient }
502
501
  ];
503
502
 
503
+ /**
504
+ * Audio analyzer for waveform visualization only.
505
+ * Do NOT use isUserSpeaking$ for call state; speaking state must come from Daily.js.
506
+ */
504
507
  class AudioAnalyzerService {
505
508
  constructor() {
506
509
  this.audioContext = null;
@@ -617,13 +620,283 @@ AudioAnalyzerService.decorators = [
617
620
  },] }
618
621
  ];
619
622
 
623
+ /**
624
+ * WebSocket-only client for voice agent signaling.
625
+ * Responsibilities:
626
+ * - Connect to ws_url
627
+ * - Parse JSON messages
628
+ * - Emit roomCreated$, userTranscript$, botTranscript$
629
+ * - NO audio logic, NO mic logic.
630
+ */
631
+ class WebSocketVoiceClientService {
632
+ constructor() {
633
+ this.ws = null;
634
+ this.roomCreatedSubject = new Subject();
635
+ this.userTranscriptSubject = new Subject();
636
+ this.botTranscriptSubject = new Subject();
637
+ /** Emits room_url when backend sends room_created. */
638
+ this.roomCreated$ = this.roomCreatedSubject.asObservable();
639
+ /** Emits user transcript updates. */
640
+ this.userTranscript$ = this.userTranscriptSubject.asObservable();
641
+ /** Emits bot transcript updates. */
642
+ this.botTranscript$ = this.botTranscriptSubject.asObservable();
643
+ }
644
+ /** Connect to signaling WebSocket. No audio over this connection. */
645
+ connect(wsUrl) {
646
+ var _a;
647
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
648
+ return;
649
+ }
650
+ if (this.ws) {
651
+ this.ws.close();
652
+ this.ws = null;
653
+ }
654
+ try {
655
+ this.ws = new WebSocket(wsUrl);
656
+ this.ws.onmessage = (event) => {
657
+ var _a;
658
+ try {
659
+ const msg = JSON.parse(event.data);
660
+ if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'room_created') {
661
+ const roomUrl = ((_a = msg.room_url) !== null && _a !== void 0 ? _a : msg.roomUrl);
662
+ if (typeof roomUrl === 'string') {
663
+ this.roomCreatedSubject.next(roomUrl);
664
+ }
665
+ }
666
+ else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'user_transcript' && typeof msg.text === 'string') {
667
+ this.userTranscriptSubject.next({
668
+ text: msg.text,
669
+ final: msg.final === true,
670
+ });
671
+ }
672
+ else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'bot_transcript' && typeof msg.text === 'string') {
673
+ this.botTranscriptSubject.next(msg.text);
674
+ }
675
+ }
676
+ catch (_b) {
677
+ // Ignore non-JSON or unknown messages
678
+ }
679
+ };
680
+ this.ws.onerror = () => {
681
+ this.disconnect();
682
+ };
683
+ this.ws.onclose = () => {
684
+ this.ws = null;
685
+ };
686
+ }
687
+ catch (err) {
688
+ console.error('WebSocketVoiceClient: connect failed', err);
689
+ this.ws = null;
690
+ throw err;
691
+ }
692
+ }
693
+ /** Disconnect and cleanup. */
694
+ disconnect() {
695
+ if (this.ws) {
696
+ this.ws.close();
697
+ this.ws = null;
698
+ }
699
+ }
700
+ /** Whether the WebSocket is open. */
701
+ get isConnected() {
702
+ var _a;
703
+ return ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN;
704
+ }
705
+ }
706
+ WebSocketVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function WebSocketVoiceClientService_Factory() { return new WebSocketVoiceClientService(); }, token: WebSocketVoiceClientService, providedIn: "root" });
707
+ WebSocketVoiceClientService.decorators = [
708
+ { type: Injectable, args: [{
709
+ providedIn: 'root',
710
+ },] }
711
+ ];
712
+
713
+ /**
714
+ * Daily.js WebRTC client for voice agent audio.
715
+ * Responsibilities:
716
+ * - Create and manage Daily CallObject
717
+ * - Join Daily room using room_url
718
+ * - Handle mic capture + speaker playback
719
+ * - Provide real-time speaking detection via active-speaker-change (primary)
720
+ * and track-started/track-stopped (fallback for immediate feedback)
721
+ * - Expose speaking$ (bot speaking), userSpeaking$ (user speaking), micMuted$
722
+ * - Expose localStream$ for waveform visualization (AudioAnalyzerService)
723
+ *
724
+ * Speaking state flips immediately when agent audio starts playing.
725
+ * If user speaks while bot is talking, state switches to listening.
726
+ */
727
+ class DailyVoiceClientService {
728
+ constructor(ngZone) {
729
+ this.ngZone = ngZone;
730
+ this.callObject = null;
731
+ this.localStream = null;
732
+ this.localSessionId = null;
733
+ this.speakingSubject = new BehaviorSubject(false);
734
+ this.userSpeakingSubject = new BehaviorSubject(false);
735
+ this.micMutedSubject = new BehaviorSubject(false);
736
+ this.localStreamSubject = new BehaviorSubject(null);
737
+ /** True when bot (remote participant) is the active speaker. */
738
+ this.speaking$ = this.speakingSubject.asObservable();
739
+ /** True when user (local participant) is the active speaker. */
740
+ this.userSpeaking$ = this.userSpeakingSubject.asObservable();
741
+ /** True when mic is muted. */
742
+ this.micMuted$ = this.micMutedSubject.asObservable();
743
+ /** Emits local mic stream for waveform visualization. */
744
+ this.localStream$ = this.localStreamSubject.asObservable();
745
+ }
746
+ /**
747
+ * Connect to Daily room. Acquires mic first for waveform, then joins with audio.
748
+ * @param roomUrl Daily room URL (from room_created)
749
+ * @param token Optional meeting token
750
+ */
751
+ connect(roomUrl, token) {
752
+ return __awaiter(this, void 0, void 0, function* () {
753
+ if (this.callObject) {
754
+ yield this.disconnect();
755
+ }
756
+ try {
757
+ // Get mic stream for both Daily and waveform (single capture)
758
+ const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
759
+ const audioTrack = stream.getAudioTracks()[0];
760
+ if (!audioTrack) {
761
+ stream.getTracks().forEach((t) => t.stop());
762
+ throw new Error('No audio track');
763
+ }
764
+ this.localStream = stream;
765
+ this.localStreamSubject.next(stream);
766
+ // Create audio-only call object
767
+ // videoSource: false = no camera, audioSource = our mic track
768
+ const callObject = Daily.createCallObject({
769
+ videoSource: false,
770
+ audioSource: audioTrack,
771
+ });
772
+ this.callObject = callObject;
773
+ this.setupEventHandlers(callObject);
774
+ // Join room; Daily handles playback of remote (bot) audio automatically
775
+ yield callObject.join({ url: roomUrl, token });
776
+ const participants = callObject.participants();
777
+ if (participants === null || participants === void 0 ? void 0 : participants.local) {
778
+ this.localSessionId = participants.local.session_id;
779
+ }
780
+ // Initial mute state: Daily starts with audio on
781
+ this.micMutedSubject.next(!callObject.localAudio());
782
+ }
783
+ catch (err) {
784
+ this.cleanup();
785
+ throw err;
786
+ }
787
+ });
788
+ }
789
+ setupEventHandlers(call) {
790
+ // active-speaker-change: primary source for real-time speaking detection.
791
+ // Emits when the loudest participant changes; bot speaking = remote is active.
792
+ call.on('active-speaker-change', (event) => {
793
+ this.ngZone.run(() => {
794
+ var _a;
795
+ const peerId = (_a = event === null || event === void 0 ? void 0 : event.activeSpeaker) === null || _a === void 0 ? void 0 : _a.peerId;
796
+ if (!peerId || !this.localSessionId) {
797
+ this.speakingSubject.next(false);
798
+ this.userSpeakingSubject.next(false);
799
+ return;
800
+ }
801
+ const isLocal = peerId === this.localSessionId;
802
+ this.userSpeakingSubject.next(isLocal);
803
+ this.speakingSubject.next(!isLocal);
804
+ });
805
+ });
806
+ // track-started / track-stopped: fallback for immediate feedback when
807
+ // remote (bot) audio track starts or stops. Ensures talking indicator
808
+ // flips as soon as agent audio begins, without waiting for active-speaker-change.
809
+ call.on('track-started', (event) => {
810
+ this.ngZone.run(() => {
811
+ var _a, _b;
812
+ const p = event === null || event === void 0 ? void 0 : event.participant;
813
+ 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;
814
+ if (p && !p.local && type === 'audio') {
815
+ this.speakingSubject.next(true);
816
+ }
817
+ });
818
+ });
819
+ call.on('track-stopped', (event) => {
820
+ this.ngZone.run(() => {
821
+ var _a, _b;
822
+ const p = event === null || event === void 0 ? void 0 : event.participant;
823
+ 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;
824
+ if (p && !p.local && type === 'audio') {
825
+ this.speakingSubject.next(false);
826
+ }
827
+ });
828
+ });
829
+ call.on('left-meeting', () => {
830
+ this.ngZone.run(() => this.cleanup());
831
+ });
832
+ call.on('error', (event) => {
833
+ this.ngZone.run(() => {
834
+ var _a;
835
+ console.error('DailyVoiceClient: Daily error', (_a = event === null || event === void 0 ? void 0 : event.errorMsg) !== null && _a !== void 0 ? _a : event);
836
+ this.cleanup();
837
+ });
838
+ });
839
+ }
840
+ /** Set mic muted state. */
841
+ setMuted(muted) {
842
+ if (!this.callObject)
843
+ return;
844
+ this.callObject.setLocalAudio(!muted);
845
+ this.micMutedSubject.next(muted);
846
+ }
847
+ /** Disconnect and cleanup. */
848
+ disconnect() {
849
+ return __awaiter(this, void 0, void 0, function* () {
850
+ if (!this.callObject) {
851
+ this.cleanup();
852
+ return;
853
+ }
854
+ try {
855
+ yield this.callObject.leave();
856
+ }
857
+ catch (e) {
858
+ // ignore
859
+ }
860
+ this.cleanup();
861
+ });
862
+ }
863
+ cleanup() {
864
+ if (this.callObject) {
865
+ this.callObject.destroy().catch(() => { });
866
+ this.callObject = null;
867
+ }
868
+ if (this.localStream) {
869
+ this.localStream.getTracks().forEach((t) => t.stop());
870
+ this.localStream = null;
871
+ }
872
+ this.localSessionId = null;
873
+ this.speakingSubject.next(false);
874
+ this.userSpeakingSubject.next(false);
875
+ this.localStreamSubject.next(null);
876
+ // Keep last micMuted state; will reset on next connect
877
+ }
878
+ }
879
+ DailyVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function DailyVoiceClientService_Factory() { return new DailyVoiceClientService(i0.ɵɵinject(i0.NgZone)); }, token: DailyVoiceClientService, providedIn: "root" });
880
+ DailyVoiceClientService.decorators = [
881
+ { type: Injectable, args: [{
882
+ providedIn: 'root',
883
+ },] }
884
+ ];
885
+ DailyVoiceClientService.ctorParameters = () => [
886
+ { type: NgZone }
887
+ ];
888
+
889
+ /**
890
+ * Voice agent orchestrator. Coordinates WebSocket (signaling) and Daily.js (WebRTC audio).
891
+ * - Maintains callState, statusText, duration, isMicMuted, isUserSpeaking, audioLevels
892
+ * - Uses WebSocket for room_created and transcripts only (no audio)
893
+ * - Uses Daily.js for all audio, mic, and real-time speaking detection
894
+ */
620
895
  class VoiceAgentService {
621
- constructor(audioAnalyzer) {
896
+ constructor(audioAnalyzer, wsClient, dailyClient) {
622
897
  this.audioAnalyzer = audioAnalyzer;
623
- this.client = null;
624
- this.transport = null;
625
- this.userMediaStream = null;
626
- this.botAudioElement = null;
898
+ this.wsClient = wsClient;
899
+ this.dailyClient = dailyClient;
627
900
  this.callStateSubject = new BehaviorSubject('idle');
628
901
  this.statusTextSubject = new BehaviorSubject('');
629
902
  this.durationSubject = new BehaviorSubject('00:00');
@@ -634,6 +907,8 @@ class VoiceAgentService {
634
907
  this.botTranscriptSubject = new Subject();
635
908
  this.callStartTime = 0;
636
909
  this.durationInterval = null;
910
+ this.subscriptions = new Subscription();
911
+ this.destroy$ = new Subject();
637
912
  this.callState$ = this.callStateSubject.asObservable();
638
913
  this.statusText$ = this.statusTextSubject.asObservable();
639
914
  this.duration$ = this.durationSubject.asObservable();
@@ -642,39 +917,23 @@ class VoiceAgentService {
642
917
  this.audioLevels$ = this.audioLevelsSubject.asObservable();
643
918
  this.userTranscript$ = this.userTranscriptSubject.asObservable();
644
919
  this.botTranscript$ = this.botTranscriptSubject.asObservable();
645
- this.audioAnalyzer.audioLevels$.subscribe(levels => {
646
- this.audioLevelsSubject.next(levels);
647
- });
648
- this.audioAnalyzer.isUserSpeaking$.subscribe(isSpeaking => {
649
- this.isUserSpeakingSubject.next(isSpeaking);
650
- if (isSpeaking && this.callStateSubject.value === 'connected') {
651
- this.callStateSubject.next('listening');
652
- }
653
- else if (!isSpeaking && this.callStateSubject.value === 'listening') {
654
- this.callStateSubject.next('connected');
655
- }
656
- });
920
+ // Waveform visualization only - do NOT use for speaking state
921
+ this.subscriptions.add(this.audioAnalyzer.audioLevels$.subscribe((levels) => this.audioLevelsSubject.next(levels)));
922
+ }
923
+ ngOnDestroy() {
924
+ this.destroy$.next();
925
+ this.subscriptions.unsubscribe();
926
+ this.disconnect();
657
927
  }
658
928
  /** Reset to idle state (e.g. when modal opens so user can click Start Call). */
659
929
  resetToIdle() {
660
930
  if (this.callStateSubject.value === 'idle')
661
931
  return;
662
- if (this.durationInterval) {
663
- clearInterval(this.durationInterval);
664
- this.durationInterval = null;
665
- }
932
+ this.stopDurationTimer();
666
933
  this.audioAnalyzer.stop();
667
- this.client = null;
668
- this.transport = null;
669
- if (this.userMediaStream) {
670
- this.userMediaStream.getTracks().forEach(track => track.stop());
671
- this.userMediaStream = null;
672
- }
673
- if (this.botAudioElement) {
674
- this.botAudioElement.pause();
675
- this.botAudioElement.srcObject = null;
676
- this.botAudioElement = null;
677
- }
934
+ this.wsClient.disconnect();
935
+ // Fire-and-forget: Daily disconnect is async; connect() will await if needed
936
+ void this.dailyClient.disconnect();
678
937
  this.callStateSubject.next('idle');
679
938
  this.statusTextSubject.next('');
680
939
  this.durationSubject.next('0:00');
@@ -690,44 +949,58 @@ class VoiceAgentService {
690
949
  this.callStateSubject.next('connecting');
691
950
  this.statusTextSubject.next('Connecting...');
692
951
  const baseUrl = apiUrl.replace(/\/$/, '');
693
- const connectUrl = `${baseUrl}/ai/ask-voice`;
694
- const headers = new Headers();
695
- headers.set('Authorization', `Bearer ${token}`);
696
- // Do not set Content-Type here — the Pipecat client already sets "application/json" once; adding it again produces "application/json, application/json"
697
- headers.set('domain-authority', domainAuthority);
698
- headers.set('eventtoken', eventToken);
699
- headers.set('eventurl', eventUrl);
700
- headers.set('hive-bot-id', botId);
701
- headers.set('x-api-key', apiKey);
702
- const startBotParams = {
703
- endpoint: connectUrl.ws_url,
952
+ const postUrl = `${baseUrl}/ai/ask-voice`;
953
+ const headers = {
954
+ 'Content-Type': 'application/json',
955
+ Authorization: `Bearer ${token}`,
956
+ 'domain-authority': domainAuthority,
957
+ 'eventtoken': eventToken,
958
+ 'eventurl': eventUrl,
959
+ 'hive-bot-id': botId,
960
+ 'x-api-key': apiKey,
961
+ };
962
+ // POST to get ws_url for signaling
963
+ const res = yield fetch(postUrl, {
964
+ method: 'POST',
704
965
  headers,
705
- requestData: {
966
+ body: JSON.stringify({
706
967
  bot_id: botId,
707
968
  conversation_id: conversationId,
708
969
  voice: 'alloy',
709
- },
710
- };
711
- this.transport = new WebSocketTransport();
712
- this.client = new PipecatClient({
713
- transport: this.transport,
714
- enableMic: true,
715
- enableCam: false,
970
+ }),
716
971
  });
717
- this.setupEventHandlers();
718
- this.botAudioElement = new Audio();
719
- this.botAudioElement.autoplay = true;
720
- yield this.client.startBotAndConnect(startBotParams);
721
- const tracks = this.client.tracks();
722
- if ((_a = tracks === null || tracks === void 0 ? void 0 : tracks.local) === null || _a === void 0 ? void 0 : _a.audio) {
723
- this.userMediaStream = new MediaStream([tracks.local.audio]);
724
- this.audioAnalyzer.start(this.userMediaStream);
972
+ if (!res.ok) {
973
+ throw new Error(`HTTP ${res.status}`);
725
974
  }
726
- this.isMicMutedSubject.next(!this.client.isMicEnabled);
727
- this.callStateSubject.next('connected');
728
- this.statusTextSubject.next('Connected');
729
- this.callStartTime = Date.now();
730
- this.startDurationTimer();
975
+ const json = yield res.json();
976
+ const wsUrl = (_a = json === null || json === void 0 ? void 0 : json.ws_url) !== null && _a !== void 0 ? _a : json === null || json === void 0 ? void 0 : json.rn_ws_url;
977
+ if (!wsUrl || typeof wsUrl !== 'string') {
978
+ throw new Error('No ws_url in response');
979
+ }
980
+ // Subscribe to room_created BEFORE connecting to avoid race
981
+ this.wsClient.roomCreated$
982
+ .pipe(take(1), takeUntil(this.destroy$))
983
+ .subscribe((roomUrl) => __awaiter(this, void 0, void 0, function* () {
984
+ try {
985
+ yield this.onRoomCreated(roomUrl);
986
+ }
987
+ catch (err) {
988
+ console.error('Daily join failed:', err);
989
+ this.callStateSubject.next('ended');
990
+ this.statusTextSubject.next('Connection failed');
991
+ yield this.disconnect();
992
+ throw err;
993
+ }
994
+ }));
995
+ // Forward transcripts from WebSocket
996
+ this.subscriptions.add(this.wsClient.userTranscript$
997
+ .pipe(takeUntil(this.destroy$))
998
+ .subscribe((t) => this.userTranscriptSubject.next(t)));
999
+ this.subscriptions.add(this.wsClient.botTranscript$
1000
+ .pipe(takeUntil(this.destroy$))
1001
+ .subscribe((t) => this.botTranscriptSubject.next(t)));
1002
+ // Connect signaling WebSocket (no audio over WS)
1003
+ this.wsClient.connect(wsUrl);
731
1004
  }
732
1005
  catch (error) {
733
1006
  console.error('Error connecting voice agent:', error);
@@ -738,83 +1011,56 @@ class VoiceAgentService {
738
1011
  }
739
1012
  });
740
1013
  }
741
- disconnect() {
1014
+ onRoomCreated(roomUrl) {
742
1015
  return __awaiter(this, void 0, void 0, function* () {
743
- try {
744
- if (this.durationInterval) {
745
- clearInterval(this.durationInterval);
746
- this.durationInterval = null;
747
- }
748
- this.audioAnalyzer.stop();
749
- if (this.client) {
750
- yield this.client.disconnect();
751
- this.client = null;
1016
+ // Connect Daily.js for WebRTC audio
1017
+ yield this.dailyClient.connect(roomUrl);
1018
+ // Waveform: use local mic stream from Daily client
1019
+ this.dailyClient.localStream$
1020
+ .pipe(filter((s) => s != null), take(1))
1021
+ .subscribe((stream) => {
1022
+ this.audioAnalyzer.start(stream);
1023
+ });
1024
+ // Speaking state from Daily only (NOT from AudioAnalyzer).
1025
+ // User speaking overrides bot talking: if user speaks while bot talks, state = listening.
1026
+ this.subscriptions.add(this.dailyClient.userSpeaking$.subscribe((s) => this.isUserSpeakingSubject.next(s)));
1027
+ this.subscriptions.add(combineLatest([
1028
+ this.dailyClient.speaking$,
1029
+ this.dailyClient.userSpeaking$,
1030
+ ]).subscribe(([bot, user]) => {
1031
+ if (user) {
1032
+ this.callStateSubject.next('listening');
752
1033
  }
753
- this.transport = null;
754
- if (this.userMediaStream) {
755
- this.userMediaStream.getTracks().forEach(track => track.stop());
756
- this.userMediaStream = null;
1034
+ else if (bot) {
1035
+ this.callStateSubject.next('talking');
757
1036
  }
758
- if (this.botAudioElement) {
759
- this.botAudioElement.pause();
760
- this.botAudioElement.srcObject = null;
761
- this.botAudioElement = null;
1037
+ else if (this.callStateSubject.value === 'talking' ||
1038
+ this.callStateSubject.value === 'listening') {
1039
+ this.callStateSubject.next('connected');
762
1040
  }
763
- this.callStateSubject.next('ended');
764
- this.statusTextSubject.next('Call Ended');
765
- // Keep current duration so "Call Ended" state can show it (e.g. "Call Ended 1:04")
766
- }
767
- catch (error) {
768
- console.error('Error disconnecting voice agent:', error);
769
- }
1041
+ }));
1042
+ this.subscriptions.add(this.dailyClient.micMuted$.subscribe((muted) => this.isMicMutedSubject.next(muted)));
1043
+ this.callStateSubject.next('connected');
1044
+ this.statusTextSubject.next('Connected');
1045
+ this.callStartTime = Date.now();
1046
+ this.startDurationTimer();
770
1047
  });
771
1048
  }
772
- toggleMic() {
773
- if (!this.client)
774
- return;
775
- const newState = !this.client.isMicEnabled;
776
- this.client.enableMic(newState);
777
- this.isMicMutedSubject.next(!newState);
778
- }
779
- setupEventHandlers() {
780
- if (!this.client)
781
- return;
782
- this.client.on(RTVIEvent.BotStartedSpeaking, () => {
783
- this.callStateSubject.next('talking');
784
- });
785
- this.client.on(RTVIEvent.BotStoppedSpeaking, () => {
786
- if (this.callStateSubject.value === 'talking') {
787
- this.callStateSubject.next('connected');
788
- }
789
- });
790
- this.client.on(RTVIEvent.TrackStarted, (track, participant) => {
791
- if (this.botAudioElement && participant && !participant.local && track.kind === 'audio') {
792
- const stream = new MediaStream([track]);
793
- this.botAudioElement.srcObject = stream;
794
- this.botAudioElement.play().catch(console.error);
795
- }
796
- });
797
- this.client.on(RTVIEvent.UserTranscript, (data) => {
798
- var _a;
799
- this.userTranscriptSubject.next({
800
- text: data.text,
801
- final: (_a = data.final) !== null && _a !== void 0 ? _a : false,
802
- });
803
- });
804
- this.client.on(RTVIEvent.BotTranscript, (data) => {
805
- if (data === null || data === void 0 ? void 0 : data.text) {
806
- this.botTranscriptSubject.next(data.text);
807
- }
808
- });
809
- this.client.on(RTVIEvent.Error, () => {
810
- this.statusTextSubject.next('Error occurred');
811
- });
812
- this.client.on(RTVIEvent.Disconnected, () => {
1049
+ disconnect() {
1050
+ return __awaiter(this, void 0, void 0, function* () {
1051
+ this.stopDurationTimer();
1052
+ this.audioAnalyzer.stop();
1053
+ // Daily first, then WebSocket
1054
+ yield this.dailyClient.disconnect();
1055
+ this.wsClient.disconnect();
813
1056
  this.callStateSubject.next('ended');
814
- this.statusTextSubject.next('Disconnected');
815
- this.disconnect();
1057
+ this.statusTextSubject.next('Call Ended');
816
1058
  });
817
1059
  }
1060
+ toggleMic() {
1061
+ const current = this.isMicMutedSubject.value;
1062
+ this.dailyClient.setMuted(!current);
1063
+ }
818
1064
  startDurationTimer() {
819
1065
  const updateDuration = () => {
820
1066
  if (this.callStartTime > 0) {
@@ -824,18 +1070,26 @@ class VoiceAgentService {
824
1070
  this.durationSubject.next(`${minutes}:${String(seconds).padStart(2, '0')}`);
825
1071
  }
826
1072
  };
827
- updateDuration(); // show 0:00 immediately
1073
+ updateDuration();
828
1074
  this.durationInterval = setInterval(updateDuration, 1000);
829
1075
  }
1076
+ stopDurationTimer() {
1077
+ if (this.durationInterval) {
1078
+ clearInterval(this.durationInterval);
1079
+ this.durationInterval = null;
1080
+ }
1081
+ }
830
1082
  }
831
- VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService)); }, token: VoiceAgentService, providedIn: "root" });
1083
+ VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService), i0.ɵɵinject(WebSocketVoiceClientService), i0.ɵɵinject(DailyVoiceClientService)); }, token: VoiceAgentService, providedIn: "root" });
832
1084
  VoiceAgentService.decorators = [
833
1085
  { type: Injectable, args: [{
834
- providedIn: 'root'
1086
+ providedIn: 'root',
835
1087
  },] }
836
1088
  ];
837
1089
  VoiceAgentService.ctorParameters = () => [
838
- { type: AudioAnalyzerService }
1090
+ { type: AudioAnalyzerService },
1091
+ { type: WebSocketVoiceClientService },
1092
+ { type: DailyVoiceClientService }
839
1093
  ];
840
1094
 
841
1095
  const VOICE_MODAL_CONFIG = new InjectionToken('VOICE_MODAL_CONFIG');
@@ -2940,7 +3194,9 @@ VoiceAgentModule.decorators = [
2940
3194
  ],
2941
3195
  providers: [
2942
3196
  VoiceAgentService,
2943
- AudioAnalyzerService
3197
+ AudioAnalyzerService,
3198
+ WebSocketVoiceClientService,
3199
+ DailyVoiceClientService
2944
3200
  ],
2945
3201
  exports: [
2946
3202
  VoiceAgentModalComponent
@@ -3211,5 +3467,5 @@ HiveGptModule.decorators = [
3211
3467
  * Generated bundle index. Do not edit.
3212
3468
  */
3213
3469
 
3214
- export { AudioAnalyzerService, ChatBotComponent, ChatDrawerComponent, HiveGptModule, VOICE_MODAL_CLOSE_CALLBACK, VOICE_MODAL_CONFIG, VoiceAgentModalComponent, VoiceAgentModule, VoiceAgentService, BotsService as ɵa, SocketService as ɵb, ConversationService as ɵc, NotificationSocket as ɵd, TranslationService as ɵe, VideoPlayerComponent as ɵf, SafeHtmlPipe as ɵg, BotHtmlEditorComponent as ɵh };
3470
+ export { AudioAnalyzerService, ChatBotComponent, ChatDrawerComponent, HiveGptModule, VOICE_MODAL_CLOSE_CALLBACK, VOICE_MODAL_CONFIG, VoiceAgentModalComponent, VoiceAgentModule, VoiceAgentService, BotsService as ɵa, SocketService as ɵb, ConversationService as ɵc, NotificationSocket as ɵd, TranslationService as ɵe, WebSocketVoiceClientService as ɵf, DailyVoiceClientService as ɵg, VideoPlayerComponent as ɵh, SafeHtmlPipe as ɵi, BotHtmlEditorComponent as ɵj };
3215
3471
  //# sourceMappingURL=hivegpt-hiveai-angular.js.map