@hivegpt/hiveai-angular 0.0.585 → 0.0.587
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 +733 -260
- 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/voice-agent/components/voice-agent-modal/voice-agent-modal.component.js +24 -17
- package/esm2015/lib/components/voice-agent/services/audio-analyzer.service.js +3 -3
- package/esm2015/lib/components/voice-agent/services/daily-voice-client.service.js +326 -0
- package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +187 -189
- package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +95 -0
- package/esm2015/lib/components/voice-agent/voice-agent.module.js +7 -3
- package/fesm2015/hivegpt-hiveai-angular.js +624 -207
- 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/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts +4 -7
- package/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts.map +1 -1
- package/lib/components/voice-agent/services/audio-analyzer.service.d.ts +2 -2
- package/lib/components/voice-agent/services/daily-voice-client.service.d.ts +70 -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 +19 -23
- 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 +2 -2
- package/lib/components/voice-agent/voice-agent.module.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -5,16 +5,15 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
|
5
5
|
import * as i0 from '@angular/core';
|
|
6
6
|
import { Injectable, InjectionToken, Inject, PLATFORM_ID, Optional, NgZone, EventEmitter, Component, Injector, Output, Input, ElementRef, ChangeDetectionStrategy, ChangeDetectorRef, Renderer2, ViewContainerRef, ViewChild, ViewChildren, NgModule, Pipe } from '@angular/core';
|
|
7
7
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
8
|
-
import { BehaviorSubject, of, throwError, Subject, Subscription } from 'rxjs';
|
|
9
|
-
import { switchMap, catchError, filter, take, map, tap } from 'rxjs/operators';
|
|
8
|
+
import { BehaviorSubject, of, throwError, Subject, Subscription, combineLatest } from 'rxjs';
|
|
9
|
+
import { switchMap, catchError, filter, take, map, takeUntil, tap } from 'rxjs/operators';
|
|
10
10
|
import { isPlatformBrowser, CommonModule, DOCUMENT } from '@angular/common';
|
|
11
11
|
import { Socket } from 'ngx-socket-io';
|
|
12
12
|
import { Validators, FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
13
13
|
import * as SpeechSDK from 'microsoft-cognitiveservices-speech-sdk';
|
|
14
14
|
import * as marked from 'marked';
|
|
15
15
|
import { __awaiter } from 'tslib';
|
|
16
|
-
import
|
|
17
|
-
import { WebSocketTransport } from '@pipecat-ai/websocket-transport';
|
|
16
|
+
import Daily from '@daily-co/daily-js';
|
|
18
17
|
import { MatIconModule } from '@angular/material/icon';
|
|
19
18
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
20
19
|
import { QuillModule } from 'ngx-quill';
|
|
@@ -685,8 +684,8 @@ BotsService.ctorParameters = () => [
|
|
|
685
684
|
];
|
|
686
685
|
|
|
687
686
|
/**
|
|
688
|
-
* Audio analyzer for waveform visualization
|
|
689
|
-
*
|
|
687
|
+
* Audio analyzer for waveform visualization only.
|
|
688
|
+
* Do NOT use isUserSpeaking$ for call state; speaking state must come from Daily.js.
|
|
690
689
|
*/
|
|
691
690
|
class AudioAnalyzerService {
|
|
692
691
|
constructor() {
|
|
@@ -807,23 +806,437 @@ AudioAnalyzerService.decorators = [
|
|
|
807
806
|
];
|
|
808
807
|
|
|
809
808
|
/**
|
|
810
|
-
*
|
|
809
|
+
* WebSocket-only client for voice agent signaling.
|
|
810
|
+
* CRITICAL: Uses native WebSocket only. NO Socket.IO, NO ngx-socket-io.
|
|
811
811
|
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
|
|
818
|
-
|
|
812
|
+
* Responsibilities:
|
|
813
|
+
* - Connect to ws_url (from POST /ai/ask-voice response)
|
|
814
|
+
* - Parse JSON messages (room_created, user_transcript, bot_transcript)
|
|
815
|
+
* - Emit roomCreated$, userTranscript$, botTranscript$
|
|
816
|
+
* - NO audio logic, NO mic logic. Audio is handled by Daily.js (WebRTC).
|
|
817
|
+
*/
|
|
818
|
+
class WebSocketVoiceClientService {
|
|
819
|
+
constructor() {
|
|
820
|
+
this.ws = null;
|
|
821
|
+
this.roomCreatedSubject = new Subject();
|
|
822
|
+
this.userTranscriptSubject = new Subject();
|
|
823
|
+
this.botTranscriptSubject = new Subject();
|
|
824
|
+
/** Emits room_url when backend sends room_created. */
|
|
825
|
+
this.roomCreated$ = this.roomCreatedSubject.asObservable();
|
|
826
|
+
/** Emits user transcript updates. */
|
|
827
|
+
this.userTranscript$ = this.userTranscriptSubject.asObservable();
|
|
828
|
+
/** Emits bot transcript updates. */
|
|
829
|
+
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
830
|
+
}
|
|
831
|
+
/** Connect to signaling WebSocket. No audio over this connection. */
|
|
832
|
+
connect(wsUrl) {
|
|
833
|
+
var _a;
|
|
834
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (this.ws) {
|
|
838
|
+
this.ws.close();
|
|
839
|
+
this.ws = null;
|
|
840
|
+
}
|
|
841
|
+
try {
|
|
842
|
+
this.ws = new WebSocket(wsUrl);
|
|
843
|
+
this.ws.onmessage = (event) => {
|
|
844
|
+
var _a;
|
|
845
|
+
try {
|
|
846
|
+
const msg = JSON.parse(event.data);
|
|
847
|
+
if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'room_created') {
|
|
848
|
+
const roomUrl = ((_a = msg.room_url) !== null && _a !== void 0 ? _a : msg.roomUrl);
|
|
849
|
+
if (typeof roomUrl === 'string') {
|
|
850
|
+
this.roomCreatedSubject.next(roomUrl);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'user_transcript' && typeof msg.text === 'string') {
|
|
854
|
+
this.userTranscriptSubject.next({
|
|
855
|
+
text: msg.text,
|
|
856
|
+
final: msg.final === true,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'bot_transcript' && typeof msg.text === 'string') {
|
|
860
|
+
this.botTranscriptSubject.next(msg.text);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
catch (_b) {
|
|
864
|
+
// Ignore non-JSON or unknown messages
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
this.ws.onerror = () => {
|
|
868
|
+
this.disconnect();
|
|
869
|
+
};
|
|
870
|
+
this.ws.onclose = () => {
|
|
871
|
+
this.ws = null;
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
catch (err) {
|
|
875
|
+
console.error('WebSocketVoiceClient: connect failed', err);
|
|
876
|
+
this.ws = null;
|
|
877
|
+
throw err;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
/** Disconnect and cleanup. */
|
|
881
|
+
disconnect() {
|
|
882
|
+
if (this.ws) {
|
|
883
|
+
this.ws.close();
|
|
884
|
+
this.ws = null;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
/** Whether the WebSocket is open. */
|
|
888
|
+
get isConnected() {
|
|
889
|
+
var _a;
|
|
890
|
+
return ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
WebSocketVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function WebSocketVoiceClientService_Factory() { return new WebSocketVoiceClientService(); }, token: WebSocketVoiceClientService, providedIn: "root" });
|
|
894
|
+
WebSocketVoiceClientService.decorators = [
|
|
895
|
+
{ type: Injectable, args: [{
|
|
896
|
+
providedIn: 'root',
|
|
897
|
+
},] }
|
|
898
|
+
];
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Daily.js WebRTC client for voice agent audio.
|
|
902
|
+
* Responsibilities:
|
|
903
|
+
* - Create and manage Daily CallObject
|
|
904
|
+
* - Join Daily room using room_url
|
|
905
|
+
* - Handle mic capture + speaker playback
|
|
906
|
+
* - Bot speaking detection via AnalyserNode on remote track (instant)
|
|
907
|
+
* - User speaking detection via active-speaker-change
|
|
908
|
+
* - Expose speaking$ (bot speaking), userSpeaking$ (user speaking), micMuted$
|
|
909
|
+
* - Expose localStream$ for waveform visualization (AudioAnalyzerService)
|
|
910
|
+
*/
|
|
911
|
+
class DailyVoiceClientService {
|
|
912
|
+
constructor(ngZone) {
|
|
913
|
+
this.ngZone = ngZone;
|
|
914
|
+
this.callObject = null;
|
|
915
|
+
this.localStream = null;
|
|
916
|
+
this.localSessionId = null;
|
|
917
|
+
/** Explicit playback of remote (bot) audio; required in some browsers. */
|
|
918
|
+
this.remoteAudioElement = null;
|
|
919
|
+
/** AnalyserNode-based remote audio monitor for instant bot speaking detection. */
|
|
920
|
+
this.remoteAudioContext = null;
|
|
921
|
+
this.remoteSpeakingRAF = null;
|
|
922
|
+
this.speakingSubject = new BehaviorSubject(false);
|
|
923
|
+
this.userSpeakingSubject = new BehaviorSubject(false);
|
|
924
|
+
this.micMutedSubject = new BehaviorSubject(false);
|
|
925
|
+
this.localStreamSubject = new BehaviorSubject(null);
|
|
926
|
+
this.firstRemoteAudioFrameSubject = new BehaviorSubject(false);
|
|
927
|
+
/** True when bot (remote participant) is the active speaker. */
|
|
928
|
+
this.speaking$ = this.speakingSubject.asObservable();
|
|
929
|
+
/** True when user (local participant) is the active speaker. */
|
|
930
|
+
this.userSpeaking$ = this.userSpeakingSubject.asObservable();
|
|
931
|
+
/** True when mic is muted. */
|
|
932
|
+
this.micMuted$ = this.micMutedSubject.asObservable();
|
|
933
|
+
/** Emits local mic stream for waveform visualization. */
|
|
934
|
+
this.localStream$ = this.localStreamSubject.asObservable();
|
|
935
|
+
/** Emits true once when first remote audio frame starts playing. */
|
|
936
|
+
this.firstRemoteAudioFrame$ = this.firstRemoteAudioFrameSubject.asObservable();
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Prompt for microphone access up front so callers can handle permission
|
|
940
|
+
* denial before creating backend room/session resources.
|
|
941
|
+
*/
|
|
942
|
+
ensureMicrophoneAccess() {
|
|
943
|
+
var _a;
|
|
944
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
945
|
+
if (!((_a = navigator === null || navigator === void 0 ? void 0 : navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.getUserMedia)) {
|
|
946
|
+
throw new Error('Microphone API unavailable');
|
|
947
|
+
}
|
|
948
|
+
const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
949
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Connect to Daily room. Acquires mic first for waveform, then joins with audio.
|
|
954
|
+
* @param roomUrl Daily room URL (from room_created)
|
|
955
|
+
* @param token Optional meeting token
|
|
956
|
+
*/
|
|
957
|
+
connect(roomUrl, token) {
|
|
958
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
959
|
+
if (this.callObject) {
|
|
960
|
+
yield this.disconnect();
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
// Get mic stream for both Daily and waveform (single capture)
|
|
964
|
+
const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
965
|
+
const audioTrack = stream.getAudioTracks()[0];
|
|
966
|
+
if (!audioTrack) {
|
|
967
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
968
|
+
throw new Error('No audio track');
|
|
969
|
+
}
|
|
970
|
+
this.localStream = stream;
|
|
971
|
+
this.localStreamSubject.next(stream);
|
|
972
|
+
// Create audio-only call object
|
|
973
|
+
// videoSource: false = no camera, audioSource = our mic track
|
|
974
|
+
const callObject = Daily.createCallObject({
|
|
975
|
+
videoSource: false,
|
|
976
|
+
audioSource: audioTrack,
|
|
977
|
+
});
|
|
978
|
+
this.callObject = callObject;
|
|
979
|
+
this.setupEventHandlers(callObject);
|
|
980
|
+
// Join room; Daily handles playback of remote (bot) audio automatically.
|
|
981
|
+
// Only pass token when it's a non-empty string (Daily rejects undefined/non-string).
|
|
982
|
+
const joinOptions = { url: roomUrl };
|
|
983
|
+
if (typeof token === 'string' && token.trim() !== '') {
|
|
984
|
+
joinOptions.token = token;
|
|
985
|
+
}
|
|
986
|
+
yield callObject.join(joinOptions);
|
|
987
|
+
console.log(`[VoiceDebug] Room connected (Daily join complete) — ${new Date().toISOString()}`);
|
|
988
|
+
const participants = callObject.participants();
|
|
989
|
+
if (participants === null || participants === void 0 ? void 0 : participants.local) {
|
|
990
|
+
this.localSessionId = participants.local.session_id;
|
|
991
|
+
}
|
|
992
|
+
// Start with mic muted; VoiceAgentService auto-unmutes after first remote audio frame.
|
|
993
|
+
callObject.setLocalAudio(false);
|
|
994
|
+
this.micMutedSubject.next(true);
|
|
995
|
+
}
|
|
996
|
+
catch (err) {
|
|
997
|
+
this.cleanup();
|
|
998
|
+
throw err;
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
setupEventHandlers(call) {
|
|
1003
|
+
// active-speaker-change: used ONLY for user speaking detection.
|
|
1004
|
+
// Bot speaking is detected by our own AnalyserNode (instant, no debounce).
|
|
1005
|
+
call.on('active-speaker-change', (event) => {
|
|
1006
|
+
this.ngZone.run(() => {
|
|
1007
|
+
var _a;
|
|
1008
|
+
const peerId = (_a = event === null || event === void 0 ? void 0 : event.activeSpeaker) === null || _a === void 0 ? void 0 : _a.peerId;
|
|
1009
|
+
if (!peerId || !this.localSessionId) {
|
|
1010
|
+
this.userSpeakingSubject.next(false);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const isLocal = peerId === this.localSessionId;
|
|
1014
|
+
this.userSpeakingSubject.next(isLocal);
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
// track-started / track-stopped: set up remote audio playback + AnalyserNode monitor.
|
|
1018
|
+
call.on('track-started', (event) => {
|
|
1019
|
+
this.ngZone.run(() => {
|
|
1020
|
+
var _a, _b, _c, _d;
|
|
1021
|
+
const p = event === null || event === void 0 ? void 0 : event.participant;
|
|
1022
|
+
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;
|
|
1023
|
+
const track = event === null || event === void 0 ? void 0 : event.track;
|
|
1024
|
+
if (p && !p.local && type === 'audio') {
|
|
1025
|
+
console.log(`[VoiceDebug] Got audio track from backend (track-started) — readyState=${track === null || track === void 0 ? void 0 : track.readyState}, muted=${track === null || track === void 0 ? void 0 : track.muted} — ${new Date().toISOString()}`);
|
|
1026
|
+
const audioTrack = track !== null && track !== void 0 ? track : (_d = (_c = p.tracks) === null || _c === void 0 ? void 0 : _c.audio) === null || _d === void 0 ? void 0 : _d.track;
|
|
1027
|
+
if (audioTrack && typeof audioTrack === 'object') {
|
|
1028
|
+
this.playRemoteTrack(audioTrack);
|
|
1029
|
+
this.monitorRemoteAudio(audioTrack);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
call.on('track-stopped', (event) => {
|
|
1035
|
+
this.ngZone.run(() => {
|
|
1036
|
+
var _a, _b;
|
|
1037
|
+
const p = event === null || event === void 0 ? void 0 : event.participant;
|
|
1038
|
+
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;
|
|
1039
|
+
if (p && !p.local && type === 'audio') {
|
|
1040
|
+
this.stopRemoteAudioMonitor();
|
|
1041
|
+
this.stopRemoteAudio();
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
call.on('left-meeting', () => {
|
|
1046
|
+
this.ngZone.run(() => this.cleanup());
|
|
1047
|
+
});
|
|
1048
|
+
call.on('error', (event) => {
|
|
1049
|
+
this.ngZone.run(() => {
|
|
1050
|
+
var _a;
|
|
1051
|
+
console.error('DailyVoiceClient: Daily error', (_a = event === null || event === void 0 ? void 0 : event.errorMsg) !== null && _a !== void 0 ? _a : event);
|
|
1052
|
+
this.cleanup();
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Play remote (bot) audio track via a dedicated audio element.
|
|
1058
|
+
* Required in many browsers where Daily's internal playback does not output to speakers.
|
|
1059
|
+
*/
|
|
1060
|
+
playRemoteTrack(track) {
|
|
1061
|
+
this.stopRemoteAudio();
|
|
1062
|
+
try {
|
|
1063
|
+
console.log(`[VoiceDebug] playRemoteTrack called — track.readyState=${track.readyState}, track.muted=${track.muted} — ${new Date().toISOString()}`);
|
|
1064
|
+
track.onunmute = () => {
|
|
1065
|
+
console.log(`[VoiceDebug] Remote audio track UNMUTED (audio data arriving) — ${new Date().toISOString()}`);
|
|
1066
|
+
};
|
|
1067
|
+
const stream = new MediaStream([track]);
|
|
1068
|
+
const audio = new Audio();
|
|
1069
|
+
audio.autoplay = true;
|
|
1070
|
+
audio.srcObject = stream;
|
|
1071
|
+
this.remoteAudioElement = audio;
|
|
1072
|
+
audio.onplaying = () => {
|
|
1073
|
+
console.log(`[VoiceDebug] Audio element PLAYING (browser started playback) — ${new Date().toISOString()}`);
|
|
1074
|
+
};
|
|
1075
|
+
let firstTimeUpdate = true;
|
|
1076
|
+
audio.ontimeupdate = () => {
|
|
1077
|
+
if (firstTimeUpdate) {
|
|
1078
|
+
firstTimeUpdate = false;
|
|
1079
|
+
console.log(`[VoiceDebug] Audio element first TIMEUPDATE (actual audio output) — ${new Date().toISOString()}`);
|
|
1080
|
+
this.firstRemoteAudioFrameSubject.next(true);
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
const p = audio.play();
|
|
1084
|
+
if (p && typeof p.then === 'function') {
|
|
1085
|
+
p.then(() => {
|
|
1086
|
+
console.log(`[VoiceDebug] audio.play() resolved — ${new Date().toISOString()}`);
|
|
1087
|
+
this.firstRemoteAudioFrameSubject.next(true);
|
|
1088
|
+
}).catch((err) => {
|
|
1089
|
+
console.warn('DailyVoiceClient: remote audio play failed (may need user gesture)', err);
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
catch (err) {
|
|
1094
|
+
console.warn('DailyVoiceClient: failed to create remote audio element', err);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Monitor remote audio track energy via AnalyserNode.
|
|
1099
|
+
* Polls at ~60fps and flips speakingSubject based on actual audio energy.
|
|
1100
|
+
*/
|
|
1101
|
+
monitorRemoteAudio(track) {
|
|
1102
|
+
this.stopRemoteAudioMonitor();
|
|
1103
|
+
try {
|
|
1104
|
+
const ctx = new AudioContext();
|
|
1105
|
+
const source = ctx.createMediaStreamSource(new MediaStream([track]));
|
|
1106
|
+
const analyser = ctx.createAnalyser();
|
|
1107
|
+
analyser.fftSize = 256;
|
|
1108
|
+
source.connect(analyser);
|
|
1109
|
+
this.remoteAudioContext = ctx;
|
|
1110
|
+
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
1111
|
+
const THRESHOLD = 5;
|
|
1112
|
+
const SILENCE_MS = 1500;
|
|
1113
|
+
let lastSoundTime = 0;
|
|
1114
|
+
let isSpeaking = false;
|
|
1115
|
+
const poll = () => {
|
|
1116
|
+
if (!this.remoteAudioContext)
|
|
1117
|
+
return;
|
|
1118
|
+
analyser.getByteFrequencyData(dataArray);
|
|
1119
|
+
let sum = 0;
|
|
1120
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
1121
|
+
sum += dataArray[i];
|
|
1122
|
+
}
|
|
1123
|
+
const avg = sum / dataArray.length;
|
|
1124
|
+
const now = Date.now();
|
|
1125
|
+
if (avg > THRESHOLD) {
|
|
1126
|
+
lastSoundTime = now;
|
|
1127
|
+
if (!isSpeaking) {
|
|
1128
|
+
isSpeaking = true;
|
|
1129
|
+
console.log(`[VoiceDebug] Bot audio energy detected (speaking=true) — avg=${avg.toFixed(1)} — ${new Date().toISOString()}`);
|
|
1130
|
+
this.ngZone.run(() => {
|
|
1131
|
+
this.userSpeakingSubject.next(false);
|
|
1132
|
+
this.speakingSubject.next(true);
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
else if (isSpeaking && now - lastSoundTime > SILENCE_MS) {
|
|
1137
|
+
isSpeaking = false;
|
|
1138
|
+
console.log(`[VoiceDebug] Bot audio silence detected (speaking=false) — ${new Date().toISOString()}`);
|
|
1139
|
+
this.ngZone.run(() => this.speakingSubject.next(false));
|
|
1140
|
+
}
|
|
1141
|
+
this.remoteSpeakingRAF = requestAnimationFrame(poll);
|
|
1142
|
+
};
|
|
1143
|
+
this.remoteSpeakingRAF = requestAnimationFrame(poll);
|
|
1144
|
+
}
|
|
1145
|
+
catch (err) {
|
|
1146
|
+
console.warn('DailyVoiceClient: failed to create remote audio monitor', err);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
stopRemoteAudioMonitor() {
|
|
1150
|
+
if (this.remoteSpeakingRAF) {
|
|
1151
|
+
cancelAnimationFrame(this.remoteSpeakingRAF);
|
|
1152
|
+
this.remoteSpeakingRAF = null;
|
|
1153
|
+
}
|
|
1154
|
+
if (this.remoteAudioContext) {
|
|
1155
|
+
this.remoteAudioContext.close().catch(() => { });
|
|
1156
|
+
this.remoteAudioContext = null;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
stopRemoteAudio() {
|
|
1160
|
+
if (this.remoteAudioElement) {
|
|
1161
|
+
try {
|
|
1162
|
+
this.remoteAudioElement.pause();
|
|
1163
|
+
this.remoteAudioElement.srcObject = null;
|
|
1164
|
+
}
|
|
1165
|
+
catch (_) { }
|
|
1166
|
+
this.remoteAudioElement = null;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
/** Set mic muted state. */
|
|
1170
|
+
setMuted(muted) {
|
|
1171
|
+
if (!this.callObject)
|
|
1172
|
+
return;
|
|
1173
|
+
this.callObject.setLocalAudio(!muted);
|
|
1174
|
+
this.micMutedSubject.next(muted);
|
|
1175
|
+
}
|
|
1176
|
+
/** Disconnect and cleanup. */
|
|
1177
|
+
disconnect() {
|
|
1178
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1179
|
+
if (!this.callObject) {
|
|
1180
|
+
this.cleanup();
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
yield this.callObject.leave();
|
|
1185
|
+
}
|
|
1186
|
+
catch (e) {
|
|
1187
|
+
// ignore
|
|
1188
|
+
}
|
|
1189
|
+
this.cleanup();
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
cleanup() {
|
|
1193
|
+
this.stopRemoteAudioMonitor();
|
|
1194
|
+
this.stopRemoteAudio();
|
|
1195
|
+
if (this.callObject) {
|
|
1196
|
+
this.callObject.destroy().catch(() => { });
|
|
1197
|
+
this.callObject = null;
|
|
1198
|
+
}
|
|
1199
|
+
if (this.localStream) {
|
|
1200
|
+
this.localStream.getTracks().forEach((t) => t.stop());
|
|
1201
|
+
this.localStream = null;
|
|
1202
|
+
}
|
|
1203
|
+
this.localSessionId = null;
|
|
1204
|
+
this.speakingSubject.next(false);
|
|
1205
|
+
this.userSpeakingSubject.next(false);
|
|
1206
|
+
this.localStreamSubject.next(null);
|
|
1207
|
+
this.firstRemoteAudioFrameSubject.next(false);
|
|
1208
|
+
// Keep last micMuted state; will reset on next connect
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
DailyVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function DailyVoiceClientService_Factory() { return new DailyVoiceClientService(i0.ɵɵinject(i0.NgZone)); }, token: DailyVoiceClientService, providedIn: "root" });
|
|
1212
|
+
DailyVoiceClientService.decorators = [
|
|
1213
|
+
{ type: Injectable, args: [{
|
|
1214
|
+
providedIn: 'root',
|
|
1215
|
+
},] }
|
|
1216
|
+
];
|
|
1217
|
+
DailyVoiceClientService.ctorParameters = () => [
|
|
1218
|
+
{ type: NgZone }
|
|
1219
|
+
];
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Voice agent orchestrator. Coordinates WebSocket (signaling) and Daily.js (WebRTC audio).
|
|
1223
|
+
*
|
|
1224
|
+
* CRITICAL: This service must NEVER use Socket.IO or ngx-socket-io. Voice flow uses only:
|
|
1225
|
+
* - Native WebSocket (WebSocketVoiceClientService) for signaling (room_created, transcripts)
|
|
1226
|
+
* - Daily.js (DailyVoiceClientService) for WebRTC audio. Audio does NOT flow over WebSocket.
|
|
1227
|
+
*
|
|
1228
|
+
* - Maintains callState, statusText, duration, isMicMuted, isUserSpeaking, audioLevels
|
|
1229
|
+
* - Uses WebSocket for room_created and transcripts only (no audio)
|
|
1230
|
+
* - Uses Daily.js for all audio, mic, and real-time speaking detection
|
|
819
1231
|
*/
|
|
820
1232
|
class VoiceAgentService {
|
|
821
|
-
constructor(audioAnalyzer,
|
|
1233
|
+
constructor(audioAnalyzer, wsClient, dailyClient, platformTokenRefresh,
|
|
822
1234
|
/** `Object` not `object` — ngc metadata collection rejects the `object` type in DI params. */
|
|
823
1235
|
platformId) {
|
|
824
1236
|
this.audioAnalyzer = audioAnalyzer;
|
|
1237
|
+
this.wsClient = wsClient;
|
|
1238
|
+
this.dailyClient = dailyClient;
|
|
825
1239
|
this.platformTokenRefresh = platformTokenRefresh;
|
|
826
|
-
this.ngZone = ngZone;
|
|
827
1240
|
this.platformId = platformId;
|
|
828
1241
|
this.callStateSubject = new BehaviorSubject('idle');
|
|
829
1242
|
this.statusTextSubject = new BehaviorSubject('');
|
|
@@ -835,10 +1248,9 @@ class VoiceAgentService {
|
|
|
835
1248
|
this.botTranscriptSubject = new Subject();
|
|
836
1249
|
this.callStartTime = 0;
|
|
837
1250
|
this.durationInterval = null;
|
|
838
|
-
this.pcClient = null;
|
|
839
|
-
this.botAudioElement = null;
|
|
840
1251
|
this.subscriptions = new Subscription();
|
|
841
1252
|
this.destroy$ = new Subject();
|
|
1253
|
+
this.hasAutoUnmutedAfterFirstAudio = false;
|
|
842
1254
|
this.callState$ = this.callStateSubject.asObservable();
|
|
843
1255
|
this.statusText$ = this.statusTextSubject.asObservable();
|
|
844
1256
|
this.duration$ = this.durationSubject.asObservable();
|
|
@@ -847,236 +1259,214 @@ class VoiceAgentService {
|
|
|
847
1259
|
this.audioLevels$ = this.audioLevelsSubject.asObservable();
|
|
848
1260
|
this.userTranscript$ = this.userTranscriptSubject.asObservable();
|
|
849
1261
|
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
1262
|
+
// Waveform visualization only - do NOT use for speaking state
|
|
850
1263
|
this.subscriptions.add(this.audioAnalyzer.audioLevels$.subscribe((levels) => this.audioLevelsSubject.next(levels)));
|
|
851
1264
|
}
|
|
852
1265
|
ngOnDestroy() {
|
|
853
1266
|
this.destroy$.next();
|
|
854
1267
|
this.subscriptions.unsubscribe();
|
|
855
|
-
|
|
1268
|
+
this.disconnect();
|
|
856
1269
|
}
|
|
857
|
-
/** Reset to idle (e.g. when modal
|
|
1270
|
+
/** Reset to idle state (e.g. when modal opens so user can click Start Call). */
|
|
858
1271
|
resetToIdle() {
|
|
859
1272
|
if (this.callStateSubject.value === 'idle')
|
|
860
1273
|
return;
|
|
861
|
-
|
|
1274
|
+
this.stopDurationTimer();
|
|
1275
|
+
this.audioAnalyzer.stop();
|
|
1276
|
+
this.wsClient.disconnect();
|
|
1277
|
+
// Fire-and-forget: Daily disconnect is async; connect() will await if needed
|
|
1278
|
+
void this.dailyClient.disconnect();
|
|
862
1279
|
this.callStateSubject.next('idle');
|
|
863
1280
|
this.statusTextSubject.next('');
|
|
864
1281
|
this.durationSubject.next('0:00');
|
|
1282
|
+
this.hasAutoUnmutedAfterFirstAudio = false;
|
|
865
1283
|
}
|
|
866
1284
|
connect(apiUrl, token, botId, conversationId, apiKey, eventToken, eventId, eventUrl, domainAuthority, usersApiUrl) {
|
|
867
1285
|
return __awaiter(this, void 0, void 0, function* () {
|
|
868
1286
|
if (this.callStateSubject.value !== 'idle') {
|
|
869
|
-
console.warn('
|
|
1287
|
+
console.warn('Call already in progress');
|
|
870
1288
|
return;
|
|
871
1289
|
}
|
|
872
1290
|
try {
|
|
873
1291
|
this.callStateSubject.next('connecting');
|
|
874
1292
|
this.statusTextSubject.next('Connecting...');
|
|
1293
|
+
// Request mic permission before provisioning backend room flow.
|
|
1294
|
+
yield this.dailyClient.ensureMicrophoneAccess();
|
|
875
1295
|
let accessToken = token;
|
|
1296
|
+
// Align with chat drawer token handling: always delegate to
|
|
1297
|
+
// PlatformTokenRefreshService when we have a usersApiUrl, so it can
|
|
1298
|
+
// fall back to stored tokens even if the caller passed an empty token.
|
|
876
1299
|
if (usersApiUrl && isPlatformBrowser(this.platformId)) {
|
|
877
1300
|
try {
|
|
878
1301
|
const ensured = yield this.platformTokenRefresh
|
|
879
1302
|
.ensureValidAccessToken(token, usersApiUrl)
|
|
880
1303
|
.pipe(take(1))
|
|
881
1304
|
.toPromise();
|
|
882
|
-
if (ensured === null || ensured === void 0 ? void 0 : ensured.accessToken)
|
|
1305
|
+
if (ensured === null || ensured === void 0 ? void 0 : ensured.accessToken) {
|
|
883
1306
|
accessToken = ensured.accessToken;
|
|
1307
|
+
}
|
|
884
1308
|
}
|
|
885
1309
|
catch (e) {
|
|
886
|
-
console.warn('[HiveGpt Voice] Token refresh failed', e);
|
|
1310
|
+
console.warn('[HiveGpt Voice] Token refresh before connect failed', e);
|
|
887
1311
|
}
|
|
888
1312
|
}
|
|
889
1313
|
const baseUrl = apiUrl.replace(/\/$/, '');
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
},
|
|
908
|
-
});
|
|
909
|
-
this.pcClient = pcClient;
|
|
910
|
-
// Bot audio arrives as a MediaStreamTrack — wire to a hidden <audio> element
|
|
911
|
-
pcClient.on(RTVIEvent.TrackStarted, (track, participant) => {
|
|
912
|
-
if (!(participant === null || participant === void 0 ? void 0 : participant.local) && track.kind === 'audio') {
|
|
913
|
-
this.ngZone.run(() => this.setupBotAudioTrack(track));
|
|
914
|
-
}
|
|
915
|
-
});
|
|
916
|
-
// Speaking state comes straight from RTVI events
|
|
917
|
-
pcClient.on(RTVIEvent.BotStartedSpeaking, () => this.ngZone.run(() => this.onBotStartedSpeaking()));
|
|
918
|
-
pcClient.on(RTVIEvent.BotStoppedSpeaking, () => this.ngZone.run(() => this.onBotStoppedSpeaking()));
|
|
919
|
-
pcClient.on(RTVIEvent.UserStartedSpeaking, () => this.ngZone.run(() => {
|
|
920
|
-
this.isUserSpeakingSubject.next(true);
|
|
921
|
-
this.callStateSubject.next('listening');
|
|
922
|
-
this.statusTextSubject.next('Listening...');
|
|
923
|
-
}));
|
|
924
|
-
pcClient.on(RTVIEvent.UserStoppedSpeaking, () => this.ngZone.run(() => {
|
|
925
|
-
this.isUserSpeakingSubject.next(false);
|
|
926
|
-
if (this.callStateSubject.value === 'listening') {
|
|
927
|
-
// Brief 'Processing...' while we wait for the bot to respond.
|
|
928
|
-
this.callStateSubject.next('connected');
|
|
929
|
-
this.statusTextSubject.next('Processing...');
|
|
930
|
-
}
|
|
931
|
-
}));
|
|
932
|
-
// Acquire mic (triggers browser permission prompt)
|
|
933
|
-
yield pcClient.initDevices();
|
|
934
|
-
// Build headers using the browser Headers API (required by pipecat's APIRequest type)
|
|
935
|
-
const requestHeaders = new Headers();
|
|
936
|
-
requestHeaders.append('Authorization', `Bearer ${accessToken}`);
|
|
937
|
-
requestHeaders.append('x-api-key', apiKey);
|
|
938
|
-
requestHeaders.append('hive-bot-id', botId);
|
|
939
|
-
requestHeaders.append('domain-authority', domainAuthority);
|
|
940
|
-
requestHeaders.append('eventUrl', eventUrl);
|
|
941
|
-
requestHeaders.append('eventId', eventId);
|
|
942
|
-
requestHeaders.append('eventToken', eventToken);
|
|
943
|
-
requestHeaders.append('ngrok-skip-browser-warning', 'true');
|
|
944
|
-
// POST to /ai/ask-voice-socket → receives { ws_url } → WebSocketTransport connects
|
|
945
|
-
yield pcClient.startBotAndConnect({
|
|
946
|
-
endpoint: `${baseUrl}/ai/ask-voice-socket`,
|
|
947
|
-
headers: requestHeaders,
|
|
948
|
-
requestData: {
|
|
1314
|
+
const postUrl = `${baseUrl}/ai/ask-voice`;
|
|
1315
|
+
const headers = {
|
|
1316
|
+
'Content-Type': 'application/json',
|
|
1317
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1318
|
+
'x-api-key': apiKey,
|
|
1319
|
+
'hive-bot-id': botId,
|
|
1320
|
+
'domain-authority': domainAuthority,
|
|
1321
|
+
eventUrl,
|
|
1322
|
+
eventId,
|
|
1323
|
+
eventToken,
|
|
1324
|
+
'ngrok-skip-browser-warning': 'true',
|
|
1325
|
+
};
|
|
1326
|
+
// POST to get ws_url for signaling
|
|
1327
|
+
const res = yield fetch(postUrl, {
|
|
1328
|
+
method: 'POST',
|
|
1329
|
+
headers,
|
|
1330
|
+
body: JSON.stringify({
|
|
949
1331
|
bot_id: botId,
|
|
950
1332
|
conversation_id: conversationId,
|
|
951
1333
|
voice: 'alloy',
|
|
952
|
-
},
|
|
1334
|
+
}),
|
|
953
1335
|
});
|
|
1336
|
+
if (!res.ok) {
|
|
1337
|
+
throw new Error(`HTTP ${res.status}`);
|
|
1338
|
+
}
|
|
1339
|
+
const json = yield res.json();
|
|
1340
|
+
const wsUrl = json === null || json === void 0 ? void 0 : json.rn_ws_url;
|
|
1341
|
+
if (!wsUrl || typeof wsUrl !== 'string') {
|
|
1342
|
+
throw new Error('No ws_url in response');
|
|
1343
|
+
}
|
|
1344
|
+
// Subscribe to room_created BEFORE connecting to avoid race
|
|
1345
|
+
this.wsClient.roomCreated$
|
|
1346
|
+
.pipe(take(1), takeUntil(this.destroy$))
|
|
1347
|
+
.subscribe((roomUrl) => __awaiter(this, void 0, void 0, function* () {
|
|
1348
|
+
try {
|
|
1349
|
+
yield this.onRoomCreated(roomUrl);
|
|
1350
|
+
}
|
|
1351
|
+
catch (err) {
|
|
1352
|
+
console.error('Daily join failed:', err);
|
|
1353
|
+
if (this.isMicPermissionError(err)) {
|
|
1354
|
+
yield this.cleanupMediaAndSignal();
|
|
1355
|
+
this.callStateSubject.next('idle');
|
|
1356
|
+
this.durationSubject.next('0:00');
|
|
1357
|
+
this.statusTextSubject.next('Microphone permission is required');
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
this.callStateSubject.next('ended');
|
|
1361
|
+
this.statusTextSubject.next('Connection failed');
|
|
1362
|
+
yield this.disconnect();
|
|
1363
|
+
}
|
|
1364
|
+
}));
|
|
1365
|
+
// Forward transcripts from WebSocket
|
|
1366
|
+
this.subscriptions.add(this.wsClient.userTranscript$
|
|
1367
|
+
.pipe(takeUntil(this.destroy$))
|
|
1368
|
+
.subscribe((t) => this.userTranscriptSubject.next(t)));
|
|
1369
|
+
this.subscriptions.add(this.wsClient.botTranscript$
|
|
1370
|
+
.pipe(takeUntil(this.destroy$))
|
|
1371
|
+
.subscribe((t) => this.botTranscriptSubject.next(t)));
|
|
1372
|
+
// Connect signaling WebSocket (no audio over WS)
|
|
1373
|
+
this.wsClient.connect(wsUrl);
|
|
954
1374
|
}
|
|
955
1375
|
catch (error) {
|
|
956
|
-
console.error('
|
|
1376
|
+
console.error('Error connecting voice agent:', error);
|
|
1377
|
+
if (this.isMicPermissionError(error)) {
|
|
1378
|
+
yield this.cleanupMediaAndSignal();
|
|
1379
|
+
this.callStateSubject.next('idle');
|
|
1380
|
+
this.durationSubject.next('0:00');
|
|
1381
|
+
this.statusTextSubject.next('Microphone permission is required');
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
957
1384
|
this.callStateSubject.next('ended');
|
|
958
|
-
yield this.
|
|
1385
|
+
yield this.disconnect();
|
|
959
1386
|
this.statusTextSubject.next('Connection failed');
|
|
960
1387
|
throw error;
|
|
961
1388
|
}
|
|
962
1389
|
});
|
|
963
1390
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
this.
|
|
1014
|
-
|
|
1015
|
-
}
|
|
1016
|
-
const existing = (_a = this.botAudioElement.srcObject) === null || _a === void 0 ? void 0 : _a.getAudioTracks()[0];
|
|
1017
|
-
if ((existing === null || existing === void 0 ? void 0 : existing.id) === track.id)
|
|
1018
|
-
return;
|
|
1019
|
-
this.botAudioElement.srcObject = new MediaStream([track]);
|
|
1020
|
-
this.botAudioElement.play().catch((err) => console.warn('[HiveGpt Voice] Bot audio play blocked', err));
|
|
1021
|
-
}
|
|
1022
|
-
stopBotAudio() {
|
|
1023
|
-
var _a;
|
|
1024
|
-
if (this.botAudioElement) {
|
|
1025
|
-
try {
|
|
1026
|
-
this.botAudioElement.pause();
|
|
1027
|
-
(_a = this.botAudioElement.srcObject) === null || _a === void 0 ? void 0 : _a.getAudioTracks().forEach((t) => t.stop());
|
|
1028
|
-
this.botAudioElement.srcObject = null;
|
|
1029
|
-
}
|
|
1030
|
-
catch (_b) {
|
|
1031
|
-
// ignore
|
|
1032
|
-
}
|
|
1033
|
-
this.botAudioElement = null;
|
|
1034
|
-
}
|
|
1391
|
+
onRoomCreated(roomUrl) {
|
|
1392
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1393
|
+
// Connect Daily.js for WebRTC audio
|
|
1394
|
+
yield this.dailyClient.connect(roomUrl);
|
|
1395
|
+
this.hasAutoUnmutedAfterFirstAudio = false;
|
|
1396
|
+
// Waveform: use local mic stream from Daily client
|
|
1397
|
+
this.dailyClient.localStream$
|
|
1398
|
+
.pipe(filter((s) => s != null), take(1))
|
|
1399
|
+
.subscribe((stream) => {
|
|
1400
|
+
this.audioAnalyzer.start(stream);
|
|
1401
|
+
});
|
|
1402
|
+
this.subscriptions.add(this.dailyClient.userSpeaking$.subscribe((s) => this.isUserSpeakingSubject.next(s)));
|
|
1403
|
+
this.subscriptions.add(combineLatest([
|
|
1404
|
+
this.dailyClient.speaking$,
|
|
1405
|
+
this.dailyClient.userSpeaking$,
|
|
1406
|
+
]).subscribe(([bot, user]) => {
|
|
1407
|
+
const current = this.callStateSubject.value;
|
|
1408
|
+
if (current === 'connecting' && !bot) {
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (current === 'connecting' && bot) {
|
|
1412
|
+
this.callStartTime = Date.now();
|
|
1413
|
+
this.startDurationTimer();
|
|
1414
|
+
this.callStateSubject.next('talking');
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (user) {
|
|
1418
|
+
this.callStateSubject.next('listening');
|
|
1419
|
+
}
|
|
1420
|
+
else if (bot) {
|
|
1421
|
+
this.callStateSubject.next('talking');
|
|
1422
|
+
}
|
|
1423
|
+
else if (current === 'talking' || current === 'listening') {
|
|
1424
|
+
this.callStateSubject.next('connected');
|
|
1425
|
+
}
|
|
1426
|
+
}));
|
|
1427
|
+
this.subscriptions.add(this.dailyClient.micMuted$.subscribe((muted) => this.isMicMutedSubject.next(muted)));
|
|
1428
|
+
// One-time auto-unmute after first remote audio frame starts playing.
|
|
1429
|
+
// This keeps initial capture muted until bot audio is heard, then restores normal mic flow.
|
|
1430
|
+
this.subscriptions.add(this.dailyClient.firstRemoteAudioFrame$
|
|
1431
|
+
.pipe(filter((hasFirstFrame) => hasFirstFrame), take(1))
|
|
1432
|
+
.subscribe(() => {
|
|
1433
|
+
if (this.hasAutoUnmutedAfterFirstAudio)
|
|
1434
|
+
return;
|
|
1435
|
+
this.hasAutoUnmutedAfterFirstAudio = true;
|
|
1436
|
+
if (this.isMicMutedSubject.value) {
|
|
1437
|
+
this.dailyClient.setMuted(false);
|
|
1438
|
+
}
|
|
1439
|
+
}));
|
|
1440
|
+
this.statusTextSubject.next('Connecting...');
|
|
1441
|
+
});
|
|
1035
1442
|
}
|
|
1036
1443
|
disconnect() {
|
|
1037
1444
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1038
1445
|
this.stopDurationTimer();
|
|
1039
|
-
this.callStartTime = 0;
|
|
1040
1446
|
this.audioAnalyzer.stop();
|
|
1041
|
-
|
|
1042
|
-
yield this.
|
|
1447
|
+
// Daily first, then WebSocket
|
|
1448
|
+
yield this.dailyClient.disconnect();
|
|
1449
|
+
this.wsClient.disconnect();
|
|
1043
1450
|
this.callStateSubject.next('ended');
|
|
1044
1451
|
this.statusTextSubject.next('Call Ended');
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
cleanupPipecatClient() {
|
|
1048
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
1049
|
-
if (this.pcClient) {
|
|
1050
|
-
try {
|
|
1051
|
-
yield this.pcClient.disconnect();
|
|
1052
|
-
}
|
|
1053
|
-
catch (_a) {
|
|
1054
|
-
// ignore
|
|
1055
|
-
}
|
|
1056
|
-
this.pcClient = null;
|
|
1057
|
-
}
|
|
1452
|
+
this.hasAutoUnmutedAfterFirstAudio = false;
|
|
1058
1453
|
});
|
|
1059
1454
|
}
|
|
1060
1455
|
toggleMic() {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const nextMuted = !this.isMicMutedSubject.value;
|
|
1064
|
-
this.pcClient.enableMic(!nextMuted);
|
|
1065
|
-
this.isMicMutedSubject.next(nextMuted);
|
|
1066
|
-
if (nextMuted)
|
|
1067
|
-
this.isUserSpeakingSubject.next(false);
|
|
1456
|
+
const current = this.isMicMutedSubject.value;
|
|
1457
|
+
this.dailyClient.setMuted(!current);
|
|
1068
1458
|
}
|
|
1069
1459
|
startDurationTimer() {
|
|
1070
|
-
const
|
|
1460
|
+
const updateDuration = () => {
|
|
1071
1461
|
if (this.callStartTime > 0) {
|
|
1072
1462
|
const elapsed = Math.floor((Date.now() - this.callStartTime) / 1000);
|
|
1073
|
-
const
|
|
1074
|
-
const
|
|
1075
|
-
this.durationSubject.next(`${
|
|
1463
|
+
const minutes = Math.floor(elapsed / 60);
|
|
1464
|
+
const seconds = elapsed % 60;
|
|
1465
|
+
this.durationSubject.next(`${minutes}:${String(seconds).padStart(2, '0')}`);
|
|
1076
1466
|
}
|
|
1077
1467
|
};
|
|
1078
|
-
|
|
1079
|
-
this.durationInterval = setInterval(
|
|
1468
|
+
updateDuration();
|
|
1469
|
+
this.durationInterval = setInterval(updateDuration, 1000);
|
|
1080
1470
|
}
|
|
1081
1471
|
stopDurationTimer() {
|
|
1082
1472
|
if (this.durationInterval) {
|
|
@@ -1084,8 +1474,25 @@ class VoiceAgentService {
|
|
|
1084
1474
|
this.durationInterval = null;
|
|
1085
1475
|
}
|
|
1086
1476
|
}
|
|
1477
|
+
isMicPermissionError(error) {
|
|
1478
|
+
if (!error || typeof error !== 'object')
|
|
1479
|
+
return false;
|
|
1480
|
+
const maybe = error;
|
|
1481
|
+
return (maybe.name === 'NotAllowedError' ||
|
|
1482
|
+
maybe.name === 'PermissionDeniedError' ||
|
|
1483
|
+
maybe.name === 'SecurityError');
|
|
1484
|
+
}
|
|
1485
|
+
cleanupMediaAndSignal() {
|
|
1486
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1487
|
+
this.stopDurationTimer();
|
|
1488
|
+
this.audioAnalyzer.stop();
|
|
1489
|
+
yield this.dailyClient.disconnect();
|
|
1490
|
+
this.wsClient.disconnect();
|
|
1491
|
+
this.hasAutoUnmutedAfterFirstAudio = false;
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1087
1494
|
}
|
|
1088
|
-
VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService), i0.ɵɵinject(
|
|
1495
|
+
VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService), i0.ɵɵinject(WebSocketVoiceClientService), i0.ɵɵinject(DailyVoiceClientService), i0.ɵɵinject(PlatformTokenRefreshService), i0.ɵɵinject(i0.PLATFORM_ID)); }, token: VoiceAgentService, providedIn: "root" });
|
|
1089
1496
|
VoiceAgentService.decorators = [
|
|
1090
1497
|
{ type: Injectable, args: [{
|
|
1091
1498
|
providedIn: 'root',
|
|
@@ -1093,8 +1500,9 @@ VoiceAgentService.decorators = [
|
|
|
1093
1500
|
];
|
|
1094
1501
|
VoiceAgentService.ctorParameters = () => [
|
|
1095
1502
|
{ type: AudioAnalyzerService },
|
|
1503
|
+
{ type: WebSocketVoiceClientService },
|
|
1504
|
+
{ type: DailyVoiceClientService },
|
|
1096
1505
|
{ type: PlatformTokenRefreshService },
|
|
1097
|
-
{ type: NgZone },
|
|
1098
1506
|
{ type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }
|
|
1099
1507
|
];
|
|
1100
1508
|
|
|
@@ -1125,15 +1533,12 @@ class VoiceAgentModalComponent {
|
|
|
1125
1533
|
this.isMicMuted = false;
|
|
1126
1534
|
this.isUserSpeaking = false;
|
|
1127
1535
|
this.audioLevels = [];
|
|
1536
|
+
this.isSpeaking = false;
|
|
1537
|
+
/** Track whether call has transitioned out of initial connected state. */
|
|
1538
|
+
this.hasLeftConnectedOnce = false;
|
|
1128
1539
|
this.subscriptions = [];
|
|
1129
1540
|
this.isConnecting = false;
|
|
1130
1541
|
}
|
|
1131
|
-
/** True while the bot is speaking — drives avatar pulse animation and voice visualizer. */
|
|
1132
|
-
get isBotTalking() { return this.callState === 'talking'; }
|
|
1133
|
-
/** True while the user is actively speaking — drives waveform active color. */
|
|
1134
|
-
get isUserActive() { return this.callState === 'listening' && this.isUserSpeaking && !this.isMicMuted; }
|
|
1135
|
-
/** True during the brief processing pause between user speech and bot response. */
|
|
1136
|
-
get isProcessing() { return this.callState === 'connected' && this.statusText === 'Processing...'; }
|
|
1137
1542
|
ngOnInit() {
|
|
1138
1543
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1139
1544
|
// When opened via Overlay, config is provided by injection
|
|
@@ -1154,8 +1559,16 @@ class VoiceAgentModalComponent {
|
|
|
1154
1559
|
this.agentAvatar = this.injectedConfig.agentAvatar;
|
|
1155
1560
|
this.usersApiUrl = (_h = this.injectedConfig.usersApiUrl) !== null && _h !== void 0 ? _h : this.usersApiUrl;
|
|
1156
1561
|
}
|
|
1562
|
+
// Subscribe to observables
|
|
1157
1563
|
this.subscriptions.push(this.voiceAgentService.callState$.subscribe(state => {
|
|
1158
1564
|
this.callState = state;
|
|
1565
|
+
this.isSpeaking = state === 'talking';
|
|
1566
|
+
if (state === 'listening' || state === 'talking') {
|
|
1567
|
+
this.hasLeftConnectedOnce = true;
|
|
1568
|
+
}
|
|
1569
|
+
if (state === 'idle' || state === 'ended') {
|
|
1570
|
+
this.hasLeftConnectedOnce = false;
|
|
1571
|
+
}
|
|
1159
1572
|
}));
|
|
1160
1573
|
this.subscriptions.push(this.voiceAgentService.statusText$.subscribe(text => {
|
|
1161
1574
|
this.statusText = text;
|
|
@@ -1220,16 +1633,18 @@ class VoiceAgentModalComponent {
|
|
|
1220
1633
|
const maxHeight = 4 + envelope * 38;
|
|
1221
1634
|
return minHeight + (n / 100) * (maxHeight - minHeight);
|
|
1222
1635
|
}
|
|
1223
|
-
/** Status label for active call
|
|
1636
|
+
/** Status label for active call. */
|
|
1224
1637
|
get statusLabel() {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1638
|
+
if (this.callState === 'connecting')
|
|
1639
|
+
return this.statusText || 'Connecting...';
|
|
1640
|
+
if (this.callState === 'talking')
|
|
1641
|
+
return 'Talking...';
|
|
1642
|
+
if (this.callState === 'listening')
|
|
1643
|
+
return 'Listening';
|
|
1644
|
+
if (this.callState === 'connected') {
|
|
1645
|
+
return this.hasLeftConnectedOnce ? 'Talking...' : 'Connected';
|
|
1232
1646
|
}
|
|
1647
|
+
return this.statusText || '';
|
|
1233
1648
|
}
|
|
1234
1649
|
/** Call Again: reset to idle then start a new call. */
|
|
1235
1650
|
callAgain() {
|
|
@@ -1254,8 +1669,8 @@ class VoiceAgentModalComponent {
|
|
|
1254
1669
|
VoiceAgentModalComponent.decorators = [
|
|
1255
1670
|
{ type: Component, args: [{
|
|
1256
1671
|
selector: 'hivegpt-voice-agent-modal',
|
|
1257
|
-
template: "<div class=\"voice-agent-modal-overlay\" (click)=\"endCall()\">\n <div\n class=\"voice-container voice-agent-modal\"\n (click)=\"$event.stopPropagation()\"\n >\n <!-- Header -->\n <div class=\"header\">\n <div class=\"header-left\">\n <div class=\"header-icon\">\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M12 1C8.13 1 5 4.13 5 8V14C5 17.87 8.13 21 12 21C15.87 21 19 17.87 19 14V8C19 4.13 15.87 1 12 1Z\"\n fill=\"currentColor\"\n />\n <path\n d=\"M12 23C10.34 23 9 21.66 9 20H15C15 21.66 13.66 23 12 23Z\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n <span class=\"header-title\">Voice</span>\n </div>\n <button\n class=\"close-button\"\n (click)=\"endCall()\"\n type=\"button\"\n aria-label=\"Close\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </div>\n\n <!-- Avatar Section with glow -->\n <div class=\"avatar-section\">\n <div class=\"avatar-glow\" [class.glow-talking]=\"isBotTalking\" [class.glow-listening]=\"callState === 'listening'\"></div>\n\n <!-- Particle ring \u2014 visible while bot is talking -->\n <div *ngIf=\"isBotTalking\" class=\"particles-container\">\n <span *ngFor=\"let i of [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]\"\n class=\"particle\"\n [style.--i]=\"i\"\n [style.animationDelay]=\"(i * 0.15) + 's'\">\n </span>\n </div>\n\n <div class=\"avatar-wrapper\" [class.speaking]=\"isBotTalking\" [class.listening]=\"callState === 'listening'\">\n <img class=\"avatar-image\" [src]=\"displayAvatarUrl\" alt=\"Nia\" />\n </div>\n </div>\n\n <!-- Agent Info: Nia + Collaboration Manager AI Agent Specialist -->\n <div class=\"agent-info\">\n <div class=\"agent-name\">\n Nia\n <span class=\"ai-badge\">AI</span>\n </div>\n <p class=\"agent-role\">COP30 AI Agent </p>\n </div>\n\n <!-- Start Call (when idle only) -->\n <div *ngIf=\"callState === 'idle'\" class=\"start-call-section\">\n <p *ngIf=\"statusText === 'Connection failed'\" class=\"error-message\">\n {{ statusText }}\n </p>\n <button\n class=\"start-call-button\"\n type=\"button\"\n [disabled]=\"isConnecting\"\n (click)=\"startCall()\"\n >\n <span *ngIf=\"isConnecting\">Connecting...</span>\n <span *ngIf=\"!isConnecting && statusText === 'Connection failed'\"\n >Retry</span\n >\n <span *ngIf=\"!isConnecting && statusText !== 'Connection failed'\"\n >Start Call</span\n >\n </button>\n </div>\n\n <!-- Call Ended: status + Call Again / Back to Chat -->\n <div *ngIf=\"callState === 'ended'\" class=\"call-ended-section\">\n <p class=\"call-ended-status\">\n <span class=\"status-text\">Call Ended</span>\n <span class=\"status-timer\">{{ duration }}</span>\n </p>\n <div class=\"call-ended-controls\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"callAgain()\"\n title=\"Call Again\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n <path d=\"M3 3v5h5\" />\n </svg>\n Call Again\n </button>\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"backToChat()\"\n title=\"Back to Chat\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\" />\n </svg>\n Back to Chat\n </button>\n </div>\n </div>\n\n <!-- Status (when connecting or in-call: Talking... / Listening / Connected + timer) -->\n <div\n class=\"status-indicator status-inline\"\n *ngIf=\"callState !== 'idle' && callState !== 'ended'\"\n >\n <div *ngIf=\"callState === 'connecting'\" class=\"status-connecting\">\n <svg\n class=\"spinner\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-dasharray=\"31.416\"\n stroke-dashoffset=\"31.416\"\n >\n <animate\n attributeName=\"stroke-dasharray\"\n dur=\"2s\"\n values=\"0 31.416;15.708 15.708;0 31.416;0 31.416\"\n repeatCount=\"indefinite\"\n />\n <animate\n attributeName=\"stroke-dashoffset\"\n dur=\"2s\"\n values=\"0;-15.708;-31.416;-31.416\"\n repeatCount=\"indefinite\"\n />\n </circle>\n </svg>\n <span class=\"status-text\">{{ statusText }}</span>\n </div>\n <div\n *ngIf=\"callState !== 'connecting'\"\n class=\"status-connected status-inline-row\"\n >\n <span class=\"status-text\" [class.status-talking]=\"isBotTalking\" [class.status-listening]=\"callState === 'listening'\" [class.status-processing]=\"isProcessing\">\n {{ statusLabel }}\n </span>\n\n <!-- Animated bars \u2014 visible while bot is talking -->\n <div *ngIf=\"isBotTalking\" class=\"voice-visualizer\">\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n </div>\n\n <!-- Bouncing dots \u2014 visible during processing pause -->\n <div *ngIf=\"isProcessing\" class=\"processing-dots\">\n <span></span><span></span><span></span>\n </div>\n\n <span class=\"status-timer\">{{ duration }}</span>\n </div>\n </div>\n\n <!-- Waveform: always visible during an active call, active (coloured) when user speaks -->\n <div\n *ngIf=\"callState === 'connected' || callState === 'listening' || callState === 'talking'\"\n class=\"waveform-container\"\n >\n <div class=\"waveform-bars\">\n <div\n *ngFor=\"let level of audioLevels; let i = index\"\n class=\"waveform-bar\"\n [class.active]=\"isUserActive\"\n [style.height.px]=\"getWaveformHeight(level, i)\"\n ></div>\n </div>\n </div>\n\n <!-- Call Controls (when connected) -->\n <div\n class=\"controls\"\n *ngIf=\"\n callState === 'connected' ||\n callState === 'listening' ||\n callState === 'talking'\n \"\n >\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn mic-btn\"\n [class.muted]=\"isMicMuted\"\n (click)=\"toggleMic()\"\n type=\"button\"\n [title]=\"isMicMuted ? 'Unmute' : 'Mute'\"\n >\n <!-- Microphone icon (unmuted) -->\n <svg\n *ngIf=\"!isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n </svg>\n <!-- Microphone icon (muted) -->\n <svg\n *ngIf=\"isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n <path\n d=\"M2 2 L30 30\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">Mute</span>\n </div>\n\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn end-call-btn\"\n (click)=\"hangUp()\"\n type=\"button\"\n title=\"End Call\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">End Call</span>\n </div>\n </div>\n </div>\n</div>\n",
|
|
1258
|
-
styles: [":host{display:block}.voice-agent-modal-overlay{align-items:flex-end;backdrop-filter:blur(4px);background:rgba(0,0,0,.5);bottom:0;display:flex;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;justify-content:flex-end;left:0;padding:24px;position:fixed;right:0;top:0;z-index:99999}.voice-container.voice-agent-modal{align-items:center;animation:modalEnter .3s ease-out;background:#fff;border-radius:30px;box-shadow:0 10px 40px rgba(0,0,0,.1);display:flex;flex-direction:column;max-width:440px;min-height:600px;padding:30px;position:relative;text-align:center;width:100%}@keyframes modalEnter{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.header{justify-content:space-between;margin-bottom:5px;width:100%}.header,.header-left{align-items:center;display:flex}.header-left{gap:8px}.header-icon{align-items:center;background:#0f172a;border-radius:50%;color:#fff;display:flex;height:28px;justify-content:center;width:28px}.header-title{color:#0f172a;font-size:18px;font-weight:500}.close-button{align-items:center;background:none;border:none;color:#0f172a;cursor:pointer;display:flex;justify-content:center;padding:8px;transition:color .2s}.close-button:hover{color:#475569}.avatar-section{margin-bottom:24px;position:relative}.avatar-wrapper{align-items:center;background:#0ea5a4;background:linear-gradient(135deg,#ccfbf1,#0ea5a4);border-radius:50%;display:flex;height:180px;justify-content:center;padding:6px;position:relative;width:180px}.avatar-image{-o-object-fit:cover;border:4px solid #fff;border-radius:50%;height:100%;object-fit:cover;width:100%}.avatar-glow{background:radial-gradient(circle,rgba(14,165,164,.2) 0,transparent 70%);height:240px;left:50%;pointer-events:none;position:absolute;top:50%;transform:translate(-50%,-50%);
|
|
1672
|
+
template: "<div class=\"voice-agent-modal-overlay\" (click)=\"endCall()\">\n <div\n class=\"voice-container voice-agent-modal\"\n (click)=\"$event.stopPropagation()\"\n >\n <!-- Header -->\n <div class=\"header\">\n <div class=\"header-left\">\n <div class=\"header-icon\">\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M12 1C8.13 1 5 4.13 5 8V14C5 17.87 8.13 21 12 21C15.87 21 19 17.87 19 14V8C19 4.13 15.87 1 12 1Z\"\n fill=\"currentColor\"\n />\n <path\n d=\"M12 23C10.34 23 9 21.66 9 20H15C15 21.66 13.66 23 12 23Z\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n <span class=\"header-title\">Voice</span>\n </div>\n <button\n class=\"close-button\"\n (click)=\"endCall()\"\n type=\"button\"\n aria-label=\"Close\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </div>\n\n <!-- Avatar Section with glow -->\n <div class=\"avatar-section\">\n <div class=\"avatar-glow\"></div>\n <div class=\"avatar-wrapper\" [class.speaking]=\"isSpeaking\">\n <img class=\"avatar-image\" [src]=\"displayAvatarUrl\" alt=\"Nia\" />\n </div>\n </div>\n\n <!-- Agent Info: Nia + Collaboration Manager AI Agent Specialist -->\n <div class=\"agent-info\">\n <div class=\"agent-name\">\n Nia\n <span class=\"ai-badge\">AI</span>\n </div>\n <p class=\"agent-role\">COP30 AI Agent </p>\n </div>\n\n <!-- Start Call (when idle only) -->\n <div *ngIf=\"callState === 'idle'\" class=\"start-call-section\">\n <p *ngIf=\"statusText === 'Connection failed'\" class=\"error-message\">\n {{ statusText }}\n </p>\n <button\n class=\"start-call-button\"\n type=\"button\"\n [disabled]=\"isConnecting\"\n (click)=\"startCall()\"\n >\n <span *ngIf=\"isConnecting\">Connecting...</span>\n <span *ngIf=\"!isConnecting && statusText === 'Connection failed'\"\n >Retry</span\n >\n <span *ngIf=\"!isConnecting && statusText !== 'Connection failed'\"\n >Start Call</span\n >\n </button>\n </div>\n\n <!-- Call Ended: status + Call Again / Back to Chat -->\n <div *ngIf=\"callState === 'ended'\" class=\"call-ended-section\">\n <p class=\"call-ended-status\">\n <span class=\"status-text\">Call Ended</span>\n <span class=\"status-timer\">{{ duration }}</span>\n </p>\n <div class=\"call-ended-controls\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"callAgain()\"\n title=\"Call Again\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n <path d=\"M3 3v5h5\" />\n </svg>\n Call Again\n </button>\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"backToChat()\"\n title=\"Back to Chat\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\" />\n </svg>\n Back to Chat\n </button>\n </div>\n </div>\n\n <!-- Status (when connecting or in-call: Talking... / Listening / Connected + timer) -->\n <div\n class=\"status-indicator status-inline\"\n *ngIf=\"callState !== 'idle' && callState !== 'ended'\"\n >\n <div *ngIf=\"callState === 'connecting'\" class=\"status-connecting\">\n <svg\n class=\"spinner\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-dasharray=\"31.416\"\n stroke-dashoffset=\"31.416\"\n >\n <animate\n attributeName=\"stroke-dasharray\"\n dur=\"2s\"\n values=\"0 31.416;15.708 15.708;0 31.416;0 31.416\"\n repeatCount=\"indefinite\"\n />\n <animate\n attributeName=\"stroke-dashoffset\"\n dur=\"2s\"\n values=\"0;-15.708;-31.416;-31.416\"\n repeatCount=\"indefinite\"\n />\n </circle>\n </svg>\n <span class=\"status-text\">{{ statusText }}</span>\n </div>\n <div\n *ngIf=\"callState !== 'connecting'\"\n class=\"status-connected status-inline-row\"\n >\n <span class=\"status-text\">{{ statusLabel }}</span>\n <span class=\"status-timer\">{{ duration }}</span>\n </div>\n </div>\n\n <!-- Waveform -->\n <div\n *ngIf=\"callState === 'listening'\"\n class=\"waveform-container\"\n >\n <div class=\"waveform-bars\">\n <div\n *ngFor=\"let level of audioLevels; let i = index\"\n class=\"waveform-bar\"\n [class.active]=\"isUserSpeaking\"\n [style.height.px]=\"getWaveformHeight(level, i)\"\n ></div>\n </div>\n </div>\n\n <!-- Call Controls (when connected) -->\n <div\n class=\"controls\"\n *ngIf=\"\n callState === 'connected' ||\n callState === 'listening' ||\n callState === 'talking'\n \"\n >\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn mic-btn\"\n [class.muted]=\"isMicMuted\"\n (click)=\"toggleMic()\"\n type=\"button\"\n [title]=\"isMicMuted ? 'Unmute' : 'Mute'\"\n >\n <!-- Microphone icon (unmuted) -->\n <svg\n *ngIf=\"!isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n </svg>\n <!-- Microphone icon (muted) -->\n <svg\n *ngIf=\"isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n <path\n d=\"M2 2 L30 30\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">Mute</span>\n </div>\n\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn end-call-btn\"\n (click)=\"hangUp()\"\n type=\"button\"\n title=\"End Call\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">End Call</span>\n </div>\n </div>\n </div>\n</div>\n",
|
|
1673
|
+
styles: [":host{display:block}.voice-agent-modal-overlay{align-items:flex-end;backdrop-filter:blur(4px);background:rgba(0,0,0,.5);bottom:0;display:flex;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;justify-content:flex-end;left:0;padding:24px;position:fixed;right:0;top:0;z-index:99999}.voice-container.voice-agent-modal{align-items:center;animation:modalEnter .3s ease-out;background:#fff;border-radius:30px;box-shadow:0 10px 40px rgba(0,0,0,.1);display:flex;flex-direction:column;max-width:440px;min-height:600px;padding:30px;position:relative;text-align:center;width:100%}@keyframes modalEnter{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.header{justify-content:space-between;margin-bottom:5px;width:100%}.header,.header-left{align-items:center;display:flex}.header-left{gap:8px}.header-icon{align-items:center;background:#0f172a;border-radius:50%;color:#fff;display:flex;height:28px;justify-content:center;width:28px}.header-title{color:#0f172a;font-size:18px;font-weight:500}.close-button{align-items:center;background:none;border:none;color:#0f172a;cursor:pointer;display:flex;justify-content:center;padding:8px;transition:color .2s}.close-button:hover{color:#475569}.avatar-section{margin-bottom:24px;position:relative}.avatar-wrapper{align-items:center;background:#0ea5a4;background:linear-gradient(135deg,#ccfbf1,#0ea5a4);border-radius:50%;display:flex;height:180px;justify-content:center;padding:6px;position:relative;width:180px}.avatar-image{-o-object-fit:cover;border:4px solid #fff;border-radius:50%;height:100%;object-fit:cover;width:100%}.avatar-glow{background:radial-gradient(circle,rgba(14,165,164,.2) 0,transparent 70%);height:240px;left:50%;pointer-events:none;position:absolute;top:50%;transform:translate(-50%,-50%);width:240px;z-index:-1}.avatar-wrapper.speaking{animation:avatarPulse 2s ease-in-out infinite}@keyframes avatarPulse{0%,to{box-shadow:0 0 0 0 rgba(14,165,164,.4)}50%{box-shadow:0 0 0 15px rgba(14,165,164,0)}}.agent-info{margin-bottom:40px}.agent-name{align-items:center;color:#0f172a;display:flex;font-size:24px;font-weight:700;gap:8px;justify-content:center;margin-bottom:8px}.ai-badge{background:#0ea5a4;border-radius:6px;color:#fff;font-size:10px;font-weight:700;padding:2px 6px}.agent-role{color:#0f172a;font-size:16px;font-weight:500;margin:0}.start-call-section{align-items:center;display:flex;flex-direction:column;gap:16px;margin-bottom:24px}.error-message{color:#dc2626;font-size:14px;margin:0}.start-call-button{background:#0ea5a4;border:none;border-radius:12px;color:#fff;cursor:pointer;font-size:16px;font-weight:600;padding:14px 32px;transition:background .2s}.start-call-button:hover:not(:disabled){background:#0d9488}.start-call-button:disabled{cursor:not-allowed!important;opacity:.7}.status-indicator{justify-content:center;margin-bottom:10px}.status-connecting,.status-indicator{align-items:center;display:flex;gap:12px}.spinner{animation:spin 1s linear infinite;color:#0ea5a4}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.status-text{font-weight:400}.status-text,.status-timer{color:#0f172a;font-size:16px}.status-timer{font-weight:500}.status-connected{align-items:center;display:flex;flex-direction:column;gap:4px}.status-inline .status-inline-row{align-items:center;flex-direction:row;gap:8px}.call-ended-section{align-items:center;display:flex;flex-direction:column;gap:16px;margin-bottom:24px}.call-ended-status{align-items:center;color:#0f172a;display:flex;font-size:16px;gap:8px;justify-content:center;margin:0}.call-ended-status .status-text{font-weight:400}.call-ended-status .status-timer{font-weight:500}.call-ended-controls{align-items:center;display:flex;flex-wrap:wrap;gap:16px;justify-content:center}.action-btn{align-items:center;background:#fff;border:1px solid #e2e8f0;border-radius:24px;color:#0f172a;cursor:pointer;display:flex;font-size:14px;font-weight:500;gap:8px;padding:12px 24px;transition:background .2s ease}.action-btn:hover{background:#f8fafc}.waveform-container{margin-bottom:10px;padding:0 8px}.waveform-bars,.waveform-container{align-items:center;display:flex;height:56px;justify-content:center;width:100%}.waveform-bars{gap:2px}.waveform-bar{background:#cbd5e1;border-radius:99px;flex:0 0 2px;min-height:2px;transition:height .1s ease-out;width:2px}.waveform-bar.active{background:linear-gradient(180deg,#0ea5a4,#0d9488);box-shadow:0 0 4px rgba(14,165,164,.5)}.controls{gap:24px;width:100%}.control-btn,.controls{align-items:center;display:flex;justify-content:center}.control-btn{border:none;border-radius:50%;cursor:pointer;flex-direction:column;gap:4px;height:60px;transition:transform .2s ease;width:60px}.control-btn:hover{transform:scale(1.05)}.control-btn:active{transform:scale(.95)}.control-label{color:#0f172a;font-size:12px;font-weight:500}.mic-btn{background:#e2e8f0}.mic-btn,.mic-btn .control-label{color:#475569}.mic-btn.muted{background:#e2e8f0;color:#475569}.end-call-btn{background:#ef4444;color:#fff}.end-call-btn .control-label{color:#fff}.end-call-btn:hover{background:#dc2626}"]
|
|
1259
1674
|
},] }
|
|
1260
1675
|
];
|
|
1261
1676
|
VoiceAgentModalComponent.ctorParameters = () => [
|
|
@@ -5126,8 +5541,8 @@ ChatBotComponent.propDecorators = {
|
|
|
5126
5541
|
};
|
|
5127
5542
|
|
|
5128
5543
|
/**
|
|
5129
|
-
* Voice agent module. Uses
|
|
5130
|
-
*
|
|
5544
|
+
* Voice agent module. Uses native WebSocket + Daily.js only.
|
|
5545
|
+
* Does NOT use Socket.IO or ngx-socket-io.
|
|
5131
5546
|
*/
|
|
5132
5547
|
class VoiceAgentModule {
|
|
5133
5548
|
}
|
|
@@ -5142,6 +5557,8 @@ VoiceAgentModule.decorators = [
|
|
|
5142
5557
|
providers: [
|
|
5143
5558
|
VoiceAgentService,
|
|
5144
5559
|
AudioAnalyzerService,
|
|
5560
|
+
WebSocketVoiceClientService,
|
|
5561
|
+
DailyVoiceClientService
|
|
5145
5562
|
],
|
|
5146
5563
|
exports: [
|
|
5147
5564
|
VoiceAgentModalComponent
|
|
@@ -5412,5 +5829,5 @@ HiveGptModule.decorators = [
|
|
|
5412
5829
|
* Generated bundle index. Do not edit.
|
|
5413
5830
|
*/
|
|
5414
5831
|
|
|
5415
|
-
export { AudioAnalyzerService, ChatBotComponent, ChatDrawerComponent, HIVEGPT_AUTH_STORAGE_KEY, HiveGptModule, PlatformTokenRefreshService, VOICE_MODAL_CLOSE_CALLBACK, VOICE_MODAL_CONFIG, VoiceAgentModalComponent, VoiceAgentModule, VoiceAgentService, eClassificationType, hiveGptAuthStorageKeyFactory, BotsService as ɵa, SocketService as ɵb, ConversationService as ɵc, NotificationSocket as ɵd, TranslationService as ɵe,
|
|
5832
|
+
export { AudioAnalyzerService, ChatBotComponent, ChatDrawerComponent, HIVEGPT_AUTH_STORAGE_KEY, HiveGptModule, PlatformTokenRefreshService, VOICE_MODAL_CLOSE_CALLBACK, VOICE_MODAL_CONFIG, VoiceAgentModalComponent, VoiceAgentModule, VoiceAgentService, eClassificationType, hiveGptAuthStorageKeyFactory, 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 };
|
|
5416
5833
|
//# sourceMappingURL=hivegpt-hiveai-angular.js.map
|