@hivegpt/hiveai-angular 0.0.425 → 0.0.428

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 (29) hide show
  1. package/bundles/hivegpt-hiveai-angular.umd.js +506 -164
  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/chat-drawer/chat-drawer.component.js +22 -7
  7. package/esm2015/lib/components/voice-agent/services/audio-analyzer.service.js +5 -1
  8. package/esm2015/lib/components/voice-agent/services/daily-voice-client.service.js +181 -0
  9. package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +134 -140
  10. package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +95 -0
  11. package/esm2015/lib/components/voice-agent/voice-agent.module.js +10 -2
  12. package/fesm2015/hivegpt-hiveai-angular.js +432 -149
  13. package/fesm2015/hivegpt-hiveai-angular.js.map +1 -1
  14. package/hivegpt-hiveai-angular.d.ts +5 -3
  15. package/hivegpt-hiveai-angular.d.ts.map +1 -1
  16. package/hivegpt-hiveai-angular.metadata.json +1 -1
  17. package/lib/components/chat-drawer/chat-drawer.component.d.ts +7 -0
  18. package/lib/components/chat-drawer/chat-drawer.component.d.ts.map +1 -1
  19. package/lib/components/voice-agent/services/audio-analyzer.service.d.ts +4 -0
  20. package/lib/components/voice-agent/services/audio-analyzer.service.d.ts.map +1 -1
  21. package/lib/components/voice-agent/services/daily-voice-client.service.d.ts +48 -0
  22. package/lib/components/voice-agent/services/daily-voice-client.service.d.ts.map +1 -0
  23. package/lib/components/voice-agent/services/voice-agent.service.d.ts +24 -8
  24. package/lib/components/voice-agent/services/voice-agent.service.d.ts.map +1 -1
  25. package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts +49 -0
  26. package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts.map +1 -0
  27. package/lib/components/voice-agent/voice-agent.module.d.ts +4 -0
  28. package/lib/components/voice-agent/voice-agent.module.d.ts.map +1 -1
  29. 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,290 @@ AudioAnalyzerService.decorators = [
617
620
  },] }
618
621
  ];
619
622
 
623
+ /**
624
+ * WebSocket-only client for voice agent signaling.
625
+ * CRITICAL: Uses native WebSocket only. NO Socket.IO, NO ngx-socket-io.
626
+ *
627
+ * Responsibilities:
628
+ * - Connect to ws_url (from POST /ai/ask-voice response)
629
+ * - Parse JSON messages (room_created, user_transcript, bot_transcript)
630
+ * - Emit roomCreated$, userTranscript$, botTranscript$
631
+ * - NO audio logic, NO mic logic. Audio is handled by Daily.js (WebRTC).
632
+ */
633
+ class WebSocketVoiceClientService {
634
+ constructor() {
635
+ this.ws = null;
636
+ this.roomCreatedSubject = new Subject();
637
+ this.userTranscriptSubject = new Subject();
638
+ this.botTranscriptSubject = new Subject();
639
+ /** Emits room_url when backend sends room_created. */
640
+ this.roomCreated$ = this.roomCreatedSubject.asObservable();
641
+ /** Emits user transcript updates. */
642
+ this.userTranscript$ = this.userTranscriptSubject.asObservable();
643
+ /** Emits bot transcript updates. */
644
+ this.botTranscript$ = this.botTranscriptSubject.asObservable();
645
+ }
646
+ /** Connect to signaling WebSocket. No audio over this connection. */
647
+ connect(wsUrl) {
648
+ var _a;
649
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
650
+ return;
651
+ }
652
+ if (this.ws) {
653
+ this.ws.close();
654
+ this.ws = null;
655
+ }
656
+ try {
657
+ this.ws = new WebSocket(wsUrl);
658
+ this.ws.onmessage = (event) => {
659
+ var _a;
660
+ try {
661
+ const msg = JSON.parse(event.data);
662
+ if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'room_created') {
663
+ const roomUrl = ((_a = msg.room_url) !== null && _a !== void 0 ? _a : msg.roomUrl);
664
+ if (typeof roomUrl === 'string') {
665
+ this.roomCreatedSubject.next(roomUrl);
666
+ }
667
+ }
668
+ else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'user_transcript' && typeof msg.text === 'string') {
669
+ this.userTranscriptSubject.next({
670
+ text: msg.text,
671
+ final: msg.final === true,
672
+ });
673
+ }
674
+ else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'bot_transcript' && typeof msg.text === 'string') {
675
+ this.botTranscriptSubject.next(msg.text);
676
+ }
677
+ }
678
+ catch (_b) {
679
+ // Ignore non-JSON or unknown messages
680
+ }
681
+ };
682
+ this.ws.onerror = () => {
683
+ this.disconnect();
684
+ };
685
+ this.ws.onclose = () => {
686
+ this.ws = null;
687
+ };
688
+ }
689
+ catch (err) {
690
+ console.error('WebSocketVoiceClient: connect failed', err);
691
+ this.ws = null;
692
+ throw err;
693
+ }
694
+ }
695
+ /** Disconnect and cleanup. */
696
+ disconnect() {
697
+ if (this.ws) {
698
+ this.ws.close();
699
+ this.ws = null;
700
+ }
701
+ }
702
+ /** Whether the WebSocket is open. */
703
+ get isConnected() {
704
+ var _a;
705
+ return ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN;
706
+ }
707
+ }
708
+ WebSocketVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function WebSocketVoiceClientService_Factory() { return new WebSocketVoiceClientService(); }, token: WebSocketVoiceClientService, providedIn: "root" });
709
+ WebSocketVoiceClientService.decorators = [
710
+ { type: Injectable, args: [{
711
+ providedIn: 'root',
712
+ },] }
713
+ ];
714
+
715
+ /**
716
+ * Daily.js WebRTC client for voice agent audio.
717
+ * Responsibilities:
718
+ * - Create and manage Daily CallObject
719
+ * - Join Daily room using room_url
720
+ * - Handle mic capture + speaker playback
721
+ * - Provide real-time speaking detection via active-speaker-change (primary)
722
+ * and track-started/track-stopped (fallback for immediate feedback)
723
+ * - Expose speaking$ (bot speaking), userSpeaking$ (user speaking), micMuted$
724
+ * - Expose localStream$ for waveform visualization (AudioAnalyzerService)
725
+ *
726
+ * Speaking state flips immediately when agent audio starts playing.
727
+ * If user speaks while bot is talking, state switches to listening.
728
+ */
729
+ class DailyVoiceClientService {
730
+ constructor(ngZone) {
731
+ this.ngZone = ngZone;
732
+ this.callObject = null;
733
+ this.localStream = null;
734
+ this.localSessionId = null;
735
+ this.speakingSubject = new BehaviorSubject(false);
736
+ this.userSpeakingSubject = new BehaviorSubject(false);
737
+ this.micMutedSubject = new BehaviorSubject(false);
738
+ this.localStreamSubject = new BehaviorSubject(null);
739
+ /** True when bot (remote participant) is the active speaker. */
740
+ this.speaking$ = this.speakingSubject.asObservable();
741
+ /** True when user (local participant) is the active speaker. */
742
+ this.userSpeaking$ = this.userSpeakingSubject.asObservable();
743
+ /** True when mic is muted. */
744
+ this.micMuted$ = this.micMutedSubject.asObservable();
745
+ /** Emits local mic stream for waveform visualization. */
746
+ this.localStream$ = this.localStreamSubject.asObservable();
747
+ }
748
+ /**
749
+ * Connect to Daily room. Acquires mic first for waveform, then joins with audio.
750
+ * @param roomUrl Daily room URL (from room_created)
751
+ * @param token Optional meeting token
752
+ */
753
+ connect(roomUrl, token) {
754
+ return __awaiter(this, void 0, void 0, function* () {
755
+ if (this.callObject) {
756
+ yield this.disconnect();
757
+ }
758
+ try {
759
+ // Get mic stream for both Daily and waveform (single capture)
760
+ const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
761
+ const audioTrack = stream.getAudioTracks()[0];
762
+ if (!audioTrack) {
763
+ stream.getTracks().forEach((t) => t.stop());
764
+ throw new Error('No audio track');
765
+ }
766
+ this.localStream = stream;
767
+ this.localStreamSubject.next(stream);
768
+ // Create audio-only call object
769
+ // videoSource: false = no camera, audioSource = our mic track
770
+ const callObject = Daily.createCallObject({
771
+ videoSource: false,
772
+ audioSource: audioTrack,
773
+ });
774
+ this.callObject = callObject;
775
+ this.setupEventHandlers(callObject);
776
+ // Join room; Daily handles playback of remote (bot) audio automatically
777
+ yield callObject.join({ url: roomUrl, token });
778
+ const participants = callObject.participants();
779
+ if (participants === null || participants === void 0 ? void 0 : participants.local) {
780
+ this.localSessionId = participants.local.session_id;
781
+ }
782
+ // Initial mute state: Daily starts with audio on
783
+ this.micMutedSubject.next(!callObject.localAudio());
784
+ }
785
+ catch (err) {
786
+ this.cleanup();
787
+ throw err;
788
+ }
789
+ });
790
+ }
791
+ setupEventHandlers(call) {
792
+ // active-speaker-change: primary source for real-time speaking detection.
793
+ // Emits when the loudest participant changes; bot speaking = remote is active.
794
+ call.on('active-speaker-change', (event) => {
795
+ this.ngZone.run(() => {
796
+ var _a;
797
+ const peerId = (_a = event === null || event === void 0 ? void 0 : event.activeSpeaker) === null || _a === void 0 ? void 0 : _a.peerId;
798
+ if (!peerId || !this.localSessionId) {
799
+ this.speakingSubject.next(false);
800
+ this.userSpeakingSubject.next(false);
801
+ return;
802
+ }
803
+ const isLocal = peerId === this.localSessionId;
804
+ this.userSpeakingSubject.next(isLocal);
805
+ this.speakingSubject.next(!isLocal);
806
+ });
807
+ });
808
+ // track-started / track-stopped: fallback for immediate feedback when
809
+ // remote (bot) audio track starts or stops. Ensures talking indicator
810
+ // flips as soon as agent audio begins, without waiting for active-speaker-change.
811
+ call.on('track-started', (event) => {
812
+ this.ngZone.run(() => {
813
+ var _a, _b;
814
+ const p = event === null || event === void 0 ? void 0 : event.participant;
815
+ 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;
816
+ if (p && !p.local && type === 'audio') {
817
+ this.speakingSubject.next(true);
818
+ }
819
+ });
820
+ });
821
+ call.on('track-stopped', (event) => {
822
+ this.ngZone.run(() => {
823
+ var _a, _b;
824
+ const p = event === null || event === void 0 ? void 0 : event.participant;
825
+ 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;
826
+ if (p && !p.local && type === 'audio') {
827
+ this.speakingSubject.next(false);
828
+ }
829
+ });
830
+ });
831
+ call.on('left-meeting', () => {
832
+ this.ngZone.run(() => this.cleanup());
833
+ });
834
+ call.on('error', (event) => {
835
+ this.ngZone.run(() => {
836
+ var _a;
837
+ console.error('DailyVoiceClient: Daily error', (_a = event === null || event === void 0 ? void 0 : event.errorMsg) !== null && _a !== void 0 ? _a : event);
838
+ this.cleanup();
839
+ });
840
+ });
841
+ }
842
+ /** Set mic muted state. */
843
+ setMuted(muted) {
844
+ if (!this.callObject)
845
+ return;
846
+ this.callObject.setLocalAudio(!muted);
847
+ this.micMutedSubject.next(muted);
848
+ }
849
+ /** Disconnect and cleanup. */
850
+ disconnect() {
851
+ return __awaiter(this, void 0, void 0, function* () {
852
+ if (!this.callObject) {
853
+ this.cleanup();
854
+ return;
855
+ }
856
+ try {
857
+ yield this.callObject.leave();
858
+ }
859
+ catch (e) {
860
+ // ignore
861
+ }
862
+ this.cleanup();
863
+ });
864
+ }
865
+ cleanup() {
866
+ if (this.callObject) {
867
+ this.callObject.destroy().catch(() => { });
868
+ this.callObject = null;
869
+ }
870
+ if (this.localStream) {
871
+ this.localStream.getTracks().forEach((t) => t.stop());
872
+ this.localStream = null;
873
+ }
874
+ this.localSessionId = null;
875
+ this.speakingSubject.next(false);
876
+ this.userSpeakingSubject.next(false);
877
+ this.localStreamSubject.next(null);
878
+ // Keep last micMuted state; will reset on next connect
879
+ }
880
+ }
881
+ DailyVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function DailyVoiceClientService_Factory() { return new DailyVoiceClientService(i0.ɵɵinject(i0.NgZone)); }, token: DailyVoiceClientService, providedIn: "root" });
882
+ DailyVoiceClientService.decorators = [
883
+ { type: Injectable, args: [{
884
+ providedIn: 'root',
885
+ },] }
886
+ ];
887
+ DailyVoiceClientService.ctorParameters = () => [
888
+ { type: NgZone }
889
+ ];
890
+
891
+ /**
892
+ * Voice agent orchestrator. Coordinates WebSocket (signaling) and Daily.js (WebRTC audio).
893
+ *
894
+ * CRITICAL: This service must NEVER use Socket.IO or ngx-socket-io. Voice flow uses only:
895
+ * - Native WebSocket (WebSocketVoiceClientService) for signaling (room_created, transcripts)
896
+ * - Daily.js (DailyVoiceClientService) for WebRTC audio. Audio does NOT flow over WebSocket.
897
+ *
898
+ * - Maintains callState, statusText, duration, isMicMuted, isUserSpeaking, audioLevels
899
+ * - Uses WebSocket for room_created and transcripts only (no audio)
900
+ * - Uses Daily.js for all audio, mic, and real-time speaking detection
901
+ */
620
902
  class VoiceAgentService {
621
- constructor(audioAnalyzer) {
903
+ constructor(audioAnalyzer, wsClient, dailyClient) {
622
904
  this.audioAnalyzer = audioAnalyzer;
623
- this.client = null;
624
- this.transport = null;
625
- this.userMediaStream = null;
626
- this.botAudioElement = null;
905
+ this.wsClient = wsClient;
906
+ this.dailyClient = dailyClient;
627
907
  this.callStateSubject = new BehaviorSubject('idle');
628
908
  this.statusTextSubject = new BehaviorSubject('');
629
909
  this.durationSubject = new BehaviorSubject('00:00');
@@ -634,6 +914,8 @@ class VoiceAgentService {
634
914
  this.botTranscriptSubject = new Subject();
635
915
  this.callStartTime = 0;
636
916
  this.durationInterval = null;
917
+ this.subscriptions = new Subscription();
918
+ this.destroy$ = new Subject();
637
919
  this.callState$ = this.callStateSubject.asObservable();
638
920
  this.statusText$ = this.statusTextSubject.asObservable();
639
921
  this.duration$ = this.durationSubject.asObservable();
@@ -642,39 +924,23 @@ class VoiceAgentService {
642
924
  this.audioLevels$ = this.audioLevelsSubject.asObservable();
643
925
  this.userTranscript$ = this.userTranscriptSubject.asObservable();
644
926
  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
- });
927
+ // Waveform visualization only - do NOT use for speaking state
928
+ this.subscriptions.add(this.audioAnalyzer.audioLevels$.subscribe((levels) => this.audioLevelsSubject.next(levels)));
929
+ }
930
+ ngOnDestroy() {
931
+ this.destroy$.next();
932
+ this.subscriptions.unsubscribe();
933
+ this.disconnect();
657
934
  }
658
935
  /** Reset to idle state (e.g. when modal opens so user can click Start Call). */
659
936
  resetToIdle() {
660
937
  if (this.callStateSubject.value === 'idle')
661
938
  return;
662
- if (this.durationInterval) {
663
- clearInterval(this.durationInterval);
664
- this.durationInterval = null;
665
- }
939
+ this.stopDurationTimer();
666
940
  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
- }
941
+ this.wsClient.disconnect();
942
+ // Fire-and-forget: Daily disconnect is async; connect() will await if needed
943
+ void this.dailyClient.disconnect();
678
944
  this.callStateSubject.next('idle');
679
945
  this.statusTextSubject.next('');
680
946
  this.durationSubject.next('0:00');
@@ -690,44 +956,59 @@ class VoiceAgentService {
690
956
  this.callStateSubject.next('connecting');
691
957
  this.statusTextSubject.next('Connecting...');
692
958
  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,
959
+ const postUrl = `${baseUrl}/ai/ask-voice`;
960
+ const headers = {
961
+ 'Content-Type': 'application/json',
962
+ Authorization: `Bearer ${token}`,
963
+ 'domain-authority': domainAuthority,
964
+ 'eventtoken': eventToken,
965
+ 'eventurl': eventUrl,
966
+ 'hive-bot-id': botId,
967
+ 'x-api-key': apiKey,
968
+ };
969
+ // POST to get ws_url for signaling
970
+ const res = yield fetch(postUrl, {
971
+ method: 'POST',
704
972
  headers,
705
- requestData: {
973
+ body: JSON.stringify({
706
974
  bot_id: botId,
707
975
  conversation_id: conversationId,
708
976
  voice: 'alloy',
709
- },
710
- };
711
- this.transport = new WebSocketTransport();
712
- this.client = new PipecatClient({
713
- transport: this.transport,
714
- enableMic: true,
715
- enableCam: false,
977
+ }),
716
978
  });
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);
979
+ if (!res.ok) {
980
+ throw new Error(`HTTP ${res.status}`);
981
+ }
982
+ const json = yield res.json();
983
+ // Use only WebSocket URL. Do NOT use any Socket.IO URL (e.g. socket_io_url).
984
+ 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;
985
+ if (!wsUrl || typeof wsUrl !== 'string') {
986
+ throw new Error('No ws_url in response');
725
987
  }
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();
988
+ // Subscribe to room_created BEFORE connecting to avoid race
989
+ this.wsClient.roomCreated$
990
+ .pipe(take(1), takeUntil(this.destroy$))
991
+ .subscribe((roomUrl) => __awaiter(this, void 0, void 0, function* () {
992
+ try {
993
+ yield this.onRoomCreated(roomUrl);
994
+ }
995
+ catch (err) {
996
+ console.error('Daily join failed:', err);
997
+ this.callStateSubject.next('ended');
998
+ this.statusTextSubject.next('Connection failed');
999
+ yield this.disconnect();
1000
+ throw err;
1001
+ }
1002
+ }));
1003
+ // Forward transcripts from WebSocket
1004
+ this.subscriptions.add(this.wsClient.userTranscript$
1005
+ .pipe(takeUntil(this.destroy$))
1006
+ .subscribe((t) => this.userTranscriptSubject.next(t)));
1007
+ this.subscriptions.add(this.wsClient.botTranscript$
1008
+ .pipe(takeUntil(this.destroy$))
1009
+ .subscribe((t) => this.botTranscriptSubject.next(t)));
1010
+ // Connect signaling WebSocket (no audio over WS)
1011
+ this.wsClient.connect(wsUrl);
731
1012
  }
732
1013
  catch (error) {
733
1014
  console.error('Error connecting voice agent:', error);
@@ -738,83 +1019,56 @@ class VoiceAgentService {
738
1019
  }
739
1020
  });
740
1021
  }
741
- disconnect() {
1022
+ onRoomCreated(roomUrl) {
742
1023
  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;
1024
+ // Connect Daily.js for WebRTC audio
1025
+ yield this.dailyClient.connect(roomUrl);
1026
+ // Waveform: use local mic stream from Daily client
1027
+ this.dailyClient.localStream$
1028
+ .pipe(filter((s) => s != null), take(1))
1029
+ .subscribe((stream) => {
1030
+ this.audioAnalyzer.start(stream);
1031
+ });
1032
+ // Speaking state from Daily only (NOT from AudioAnalyzer).
1033
+ // User speaking overrides bot talking: if user speaks while bot talks, state = listening.
1034
+ this.subscriptions.add(this.dailyClient.userSpeaking$.subscribe((s) => this.isUserSpeakingSubject.next(s)));
1035
+ this.subscriptions.add(combineLatest([
1036
+ this.dailyClient.speaking$,
1037
+ this.dailyClient.userSpeaking$,
1038
+ ]).subscribe(([bot, user]) => {
1039
+ if (user) {
1040
+ this.callStateSubject.next('listening');
752
1041
  }
753
- this.transport = null;
754
- if (this.userMediaStream) {
755
- this.userMediaStream.getTracks().forEach(track => track.stop());
756
- this.userMediaStream = null;
1042
+ else if (bot) {
1043
+ this.callStateSubject.next('talking');
757
1044
  }
758
- if (this.botAudioElement) {
759
- this.botAudioElement.pause();
760
- this.botAudioElement.srcObject = null;
761
- this.botAudioElement = null;
1045
+ else if (this.callStateSubject.value === 'talking' ||
1046
+ this.callStateSubject.value === 'listening') {
1047
+ this.callStateSubject.next('connected');
762
1048
  }
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
- }
1049
+ }));
1050
+ this.subscriptions.add(this.dailyClient.micMuted$.subscribe((muted) => this.isMicMutedSubject.next(muted)));
1051
+ this.callStateSubject.next('connected');
1052
+ this.statusTextSubject.next('Connected');
1053
+ this.callStartTime = Date.now();
1054
+ this.startDurationTimer();
770
1055
  });
771
1056
  }
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, () => {
1057
+ disconnect() {
1058
+ return __awaiter(this, void 0, void 0, function* () {
1059
+ this.stopDurationTimer();
1060
+ this.audioAnalyzer.stop();
1061
+ // Daily first, then WebSocket
1062
+ yield this.dailyClient.disconnect();
1063
+ this.wsClient.disconnect();
813
1064
  this.callStateSubject.next('ended');
814
- this.statusTextSubject.next('Disconnected');
815
- this.disconnect();
1065
+ this.statusTextSubject.next('Call Ended');
816
1066
  });
817
1067
  }
1068
+ toggleMic() {
1069
+ const current = this.isMicMutedSubject.value;
1070
+ this.dailyClient.setMuted(!current);
1071
+ }
818
1072
  startDurationTimer() {
819
1073
  const updateDuration = () => {
820
1074
  if (this.callStartTime > 0) {
@@ -824,18 +1078,26 @@ class VoiceAgentService {
824
1078
  this.durationSubject.next(`${minutes}:${String(seconds).padStart(2, '0')}`);
825
1079
  }
826
1080
  };
827
- updateDuration(); // show 0:00 immediately
1081
+ updateDuration();
828
1082
  this.durationInterval = setInterval(updateDuration, 1000);
829
1083
  }
1084
+ stopDurationTimer() {
1085
+ if (this.durationInterval) {
1086
+ clearInterval(this.durationInterval);
1087
+ this.durationInterval = null;
1088
+ }
1089
+ }
830
1090
  }
831
- VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService)); }, token: VoiceAgentService, providedIn: "root" });
1091
+ 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
1092
  VoiceAgentService.decorators = [
833
1093
  { type: Injectable, args: [{
834
- providedIn: 'root'
1094
+ providedIn: 'root',
835
1095
  },] }
836
1096
  ];
837
1097
  VoiceAgentService.ctorParameters = () => [
838
- { type: AudioAnalyzerService }
1098
+ { type: AudioAnalyzerService },
1099
+ { type: WebSocketVoiceClientService },
1100
+ { type: DailyVoiceClientService }
839
1101
  ];
840
1102
 
841
1103
  const VOICE_MODAL_CONFIG = new InjectionToken('VOICE_MODAL_CONFIG');
@@ -1071,6 +1333,8 @@ class ChatDrawerComponent {
1071
1333
  this.voiceModalOverlayRef = null;
1072
1334
  this.voiceTranscriptSubscriptions = [];
1073
1335
  this.domainAuthorityValue = 'prod-lite';
1336
+ /** True after Socket has been initialized for chat (avoids connecting on voice-only usage). */
1337
+ this.chatSocketInitialized = false;
1074
1338
  this.isChatingWithAi = false;
1075
1339
  this.readAllChunks = (stream) => {
1076
1340
  const reader = stream.getReader();
@@ -1161,11 +1425,11 @@ class ChatDrawerComponent {
1161
1425
  }
1162
1426
  }
1163
1427
  }
1164
- if (changes.orgId) {
1165
- if (changes.orgId.currentValue != changes.orgId.previousValue &&
1166
- changes.orgId.currentValue) {
1167
- this.initializeSocket();
1168
- }
1428
+ // Do NOT call initializeSocket() here. Socket.IO is for chat only; we defer
1429
+ // until the user actually uses chat (send message or start new conversation)
1430
+ // so that voice-only usage never triggers Socket.IO (avoids v2/v3 mismatch error).
1431
+ if (changes.orgId && changes.orgId.currentValue != changes.orgId.previousValue) {
1432
+ this.chatSocketInitialized = false;
1169
1433
  }
1170
1434
  }
1171
1435
  ngOnInit() {
@@ -1211,6 +1475,16 @@ class ChatDrawerComponent {
1211
1475
  this.initializeSpeechRecognizer(token);
1212
1476
  });
1213
1477
  }
1478
+ /**
1479
+ * Call before chat actions so Socket.IO is used only for chat, not for voice.
1480
+ * Voice agent uses native WebSocket + Daily.js only; Socket.IO must not run during voice flow.
1481
+ */
1482
+ ensureChatSocket() {
1483
+ if (this.chatSocketInitialized || !this.orgId)
1484
+ return;
1485
+ this.chatSocketInitialized = true;
1486
+ this.initializeSocket();
1487
+ }
1214
1488
  initializeSocket() {
1215
1489
  try {
1216
1490
  this.socketService.disconnectSocketConnection();
@@ -1582,6 +1856,7 @@ class ChatDrawerComponent {
1582
1856
  if (!this.input || this.loading) {
1583
1857
  return;
1584
1858
  }
1859
+ this.ensureChatSocket();
1585
1860
  this.chatLog.push({
1586
1861
  type: 'user',
1587
1862
  message: this.processMessageForDisplay(this.input),
@@ -1605,6 +1880,7 @@ class ChatDrawerComponent {
1605
1880
  if (!inputMsg || this.loading) {
1606
1881
  return;
1607
1882
  }
1883
+ this.ensureChatSocket();
1608
1884
  try {
1609
1885
  chat.relatedListItems = [];
1610
1886
  this.cdr.detectChanges();
@@ -2629,8 +2905,9 @@ class ChatDrawerComponent {
2629
2905
  this.conversationKey = this.conversationService.getKey(this.botId, true);
2630
2906
  this.chatLog = [this.chatLog[0]];
2631
2907
  this.isChatingWithAi = false;
2908
+ this.chatSocketInitialized = false;
2632
2909
  setTimeout(() => {
2633
- this.initializeSocket();
2910
+ this.ensureChatSocket();
2634
2911
  }, 200);
2635
2912
  this.scrollToBottom();
2636
2913
  this.cdr.detectChanges();
@@ -2928,6 +3205,10 @@ ChatBotComponent.propDecorators = {
2928
3205
  isDev: [{ type: Input }]
2929
3206
  };
2930
3207
 
3208
+ /**
3209
+ * Voice agent module. Uses native WebSocket + Daily.js only.
3210
+ * Does NOT use Socket.IO or ngx-socket-io.
3211
+ */
2931
3212
  class VoiceAgentModule {
2932
3213
  }
2933
3214
  VoiceAgentModule.decorators = [
@@ -2940,7 +3221,9 @@ VoiceAgentModule.decorators = [
2940
3221
  ],
2941
3222
  providers: [
2942
3223
  VoiceAgentService,
2943
- AudioAnalyzerService
3224
+ AudioAnalyzerService,
3225
+ WebSocketVoiceClientService,
3226
+ DailyVoiceClientService
2944
3227
  ],
2945
3228
  exports: [
2946
3229
  VoiceAgentModalComponent
@@ -3211,5 +3494,5 @@ HiveGptModule.decorators = [
3211
3494
  * Generated bundle index. Do not edit.
3212
3495
  */
3213
3496
 
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 };
3497
+ 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
3498
  //# sourceMappingURL=hivegpt-hiveai-angular.js.map