@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.
- package/bundles/hivegpt-hiveai-angular.umd.js +506 -164
- package/bundles/hivegpt-hiveai-angular.umd.js.map +1 -1
- package/bundles/hivegpt-hiveai-angular.umd.min.js +1 -1
- package/bundles/hivegpt-hiveai-angular.umd.min.js.map +1 -1
- package/esm2015/hivegpt-hiveai-angular.js +6 -4
- package/esm2015/lib/components/chat-drawer/chat-drawer.component.js +22 -7
- package/esm2015/lib/components/voice-agent/services/audio-analyzer.service.js +5 -1
- package/esm2015/lib/components/voice-agent/services/daily-voice-client.service.js +181 -0
- package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +134 -140
- package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +95 -0
- package/esm2015/lib/components/voice-agent/voice-agent.module.js +10 -2
- package/fesm2015/hivegpt-hiveai-angular.js +432 -149
- package/fesm2015/hivegpt-hiveai-angular.js.map +1 -1
- package/hivegpt-hiveai-angular.d.ts +5 -3
- package/hivegpt-hiveai-angular.d.ts.map +1 -1
- package/hivegpt-hiveai-angular.metadata.json +1 -1
- package/lib/components/chat-drawer/chat-drawer.component.d.ts +7 -0
- package/lib/components/chat-drawer/chat-drawer.component.d.ts.map +1 -1
- package/lib/components/voice-agent/services/audio-analyzer.service.d.ts +4 -0
- package/lib/components/voice-agent/services/audio-analyzer.service.d.ts.map +1 -1
- package/lib/components/voice-agent/services/daily-voice-client.service.d.ts +48 -0
- package/lib/components/voice-agent/services/daily-voice-client.service.d.ts.map +1 -0
- package/lib/components/voice-agent/services/voice-agent.service.d.ts +24 -8
- package/lib/components/voice-agent/services/voice-agent.service.d.ts.map +1 -1
- package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts +49 -0
- package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts.map +1 -0
- package/lib/components/voice-agent/voice-agent.module.d.ts +4 -0
- package/lib/components/voice-agent/voice-agent.module.d.ts.map +1 -1
- 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
|
|
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.
|
|
624
|
-
this.
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
663
|
-
clearInterval(this.durationInterval);
|
|
664
|
-
this.durationInterval = null;
|
|
665
|
-
}
|
|
939
|
+
this.stopDurationTimer();
|
|
666
940
|
this.audioAnalyzer.stop();
|
|
667
|
-
this.
|
|
668
|
-
|
|
669
|
-
|
|
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
|
|
694
|
-
const headers =
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
yield
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
727
|
-
this.
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
1022
|
+
onRoomCreated(roomUrl) {
|
|
742
1023
|
return __awaiter(this, void 0, void 0, function* () {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
754
|
-
|
|
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.
|
|
759
|
-
this.
|
|
760
|
-
this.
|
|
761
|
-
this.botAudioElement = null;
|
|
1045
|
+
else if (this.callStateSubject.value === 'talking' ||
|
|
1046
|
+
this.callStateSubject.value === 'listening') {
|
|
1047
|
+
this.callStateSubject.next('connected');
|
|
762
1048
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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('
|
|
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();
|
|
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
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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.
|
|
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,
|
|
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
|