@hivegpt/hiveai-angular 0.0.580 → 0.0.582
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 +302 -509
- 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 +4 -5
- package/esm2015/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.js +17 -6
- package/esm2015/lib/components/voice-agent/services/audio-analyzer.service.js +4 -4
- package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +118 -85
- package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +114 -46
- package/esm2015/lib/components/voice-agent/voice-agent.module.js +3 -5
- package/fesm2015/hivegpt-hiveai-angular.js +240 -428
- package/fesm2015/hivegpt-hiveai-angular.js.map +1 -1
- package/hivegpt-hiveai-angular.d.ts +3 -4
- 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 +6 -2
- 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/voice-agent.service.d.ts +11 -13
- 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 +23 -20
- package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts.map +1 -1
- package/lib/components/voice-agent/voice-agent.module.d.ts +1 -1
- package/lib/components/voice-agent/voice-agent.module.d.ts.map +1 -1
- package/package.json +1 -1
- package/esm2015/lib/components/voice-agent/services/daily-voice-client.service.js +0 -305
- package/lib/components/voice-agent/services/daily-voice-client.service.d.ts +0 -62
- package/lib/components/voice-agent/services/daily-voice-client.service.d.ts.map +0 -1
|
@@ -5,15 +5,14 @@ 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, combineLatest } from 'rxjs';
|
|
9
|
-
import { switchMap, catchError, filter, take, map, takeUntil, tap } from 'rxjs/operators';
|
|
8
|
+
import { BehaviorSubject, of, throwError, Subject, Subscription, merge, concat, timer, combineLatest } from 'rxjs';
|
|
9
|
+
import { switchMap, catchError, filter, take, map, takeUntil, distinctUntilChanged, startWith, 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 Daily from '@daily-co/daily-js';
|
|
17
16
|
import { MatIconModule } from '@angular/material/icon';
|
|
18
17
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
19
18
|
import { QuillModule } from 'ngx-quill';
|
|
@@ -684,8 +683,8 @@ BotsService.ctorParameters = () => [
|
|
|
684
683
|
];
|
|
685
684
|
|
|
686
685
|
/**
|
|
687
|
-
* Audio analyzer for waveform visualization
|
|
688
|
-
*
|
|
686
|
+
* Audio analyzer for waveform visualization and local (mic) speaking detection.
|
|
687
|
+
* VoiceAgentService may combine this with WebSocket server events for call state.
|
|
689
688
|
*/
|
|
690
689
|
class AudioAnalyzerService {
|
|
691
690
|
constructor() {
|
|
@@ -701,7 +700,7 @@ class AudioAnalyzerService {
|
|
|
701
700
|
this.noiseFloorSamples = [];
|
|
702
701
|
this.NOISE_FLOOR_SAMPLE_COUNT = 30;
|
|
703
702
|
this.SPEAKING_THRESHOLD_MULTIPLIER = 2.5;
|
|
704
|
-
this.WAVEFORM_BAR_COUNT =
|
|
703
|
+
this.WAVEFORM_BAR_COUNT = 60;
|
|
705
704
|
// Amplify raw amplitude so normal speech (±10–20 units) maps to visible levels
|
|
706
705
|
this.SENSITIVITY_MULTIPLIER = 5;
|
|
707
706
|
this.audioLevels$ = this.audioLevelsSubject.asObservable();
|
|
@@ -806,69 +805,82 @@ AudioAnalyzerService.decorators = [
|
|
|
806
805
|
];
|
|
807
806
|
|
|
808
807
|
/**
|
|
809
|
-
* WebSocket
|
|
808
|
+
* Native WebSocket client for voice session (signaling, transcripts, speaking hints).
|
|
810
809
|
* CRITICAL: Uses native WebSocket only. NO Socket.IO, NO ngx-socket-io.
|
|
811
810
|
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
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).
|
|
811
|
+
* Connects to `ws_url` from `POST {baseUrl}/ai/ask-voice-socket`.
|
|
812
|
+
* Parses JSON messages for transcripts and optional assistant/user speaking flags.
|
|
817
813
|
*/
|
|
818
814
|
class WebSocketVoiceClientService {
|
|
819
|
-
constructor() {
|
|
815
|
+
constructor(ngZone) {
|
|
816
|
+
this.ngZone = ngZone;
|
|
820
817
|
this.ws = null;
|
|
821
|
-
|
|
818
|
+
/** True when {@link disconnect} initiated the close (not counted as remote close). */
|
|
819
|
+
this.closeInitiatedByClient = false;
|
|
820
|
+
this.openedSubject = new Subject();
|
|
821
|
+
this.remoteCloseSubject = new Subject();
|
|
822
822
|
this.userTranscriptSubject = new Subject();
|
|
823
823
|
this.botTranscriptSubject = new Subject();
|
|
824
|
-
|
|
825
|
-
this.
|
|
826
|
-
/**
|
|
824
|
+
this.assistantSpeakingSubject = new Subject();
|
|
825
|
+
this.serverUserSpeakingSubject = new Subject();
|
|
826
|
+
/** Fires once each time the WebSocket reaches OPEN. */
|
|
827
|
+
this.opened$ = this.openedSubject.asObservable();
|
|
828
|
+
/** Fires when the socket closes without a client-initiated {@link disconnect}. */
|
|
829
|
+
this.remoteClose$ = this.remoteCloseSubject.asObservable();
|
|
827
830
|
this.userTranscript$ = this.userTranscriptSubject.asObservable();
|
|
828
|
-
/** Emits bot transcript updates. */
|
|
829
831
|
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
832
|
+
/** Assistant/bot speaking, when the server sends explicit events (see {@link handleJsonMessage}). */
|
|
833
|
+
this.assistantSpeaking$ = this.assistantSpeakingSubject.asObservable();
|
|
834
|
+
/** User speaking from server-side VAD, if provided. */
|
|
835
|
+
this.serverUserSpeaking$ = this.serverUserSpeakingSubject.asObservable();
|
|
830
836
|
}
|
|
831
|
-
/** Connect to signaling WebSocket. No audio over this connection. */
|
|
832
837
|
connect(wsUrl) {
|
|
833
838
|
var _a;
|
|
834
839
|
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
835
840
|
return;
|
|
836
841
|
}
|
|
837
842
|
if (this.ws) {
|
|
843
|
+
this.closeInitiatedByClient = true;
|
|
838
844
|
this.ws.close();
|
|
839
|
-
this.ws = null;
|
|
840
845
|
}
|
|
841
846
|
try {
|
|
842
|
-
|
|
843
|
-
this.ws
|
|
844
|
-
|
|
847
|
+
const socket = new WebSocket(wsUrl);
|
|
848
|
+
this.ws = socket;
|
|
849
|
+
socket.onopen = () => {
|
|
850
|
+
if (this.ws !== socket)
|
|
851
|
+
return;
|
|
852
|
+
this.ngZone.run(() => this.openedSubject.next());
|
|
853
|
+
};
|
|
854
|
+
socket.onmessage = (event) => {
|
|
855
|
+
if (this.ws !== socket)
|
|
856
|
+
return;
|
|
857
|
+
if (typeof event.data !== 'string') {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
845
860
|
try {
|
|
846
861
|
const msg = JSON.parse(event.data);
|
|
847
|
-
|
|
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
|
+
this.ngZone.run(() => this.handleJsonMessage(msg));
|
|
862
863
|
}
|
|
863
|
-
catch (
|
|
864
|
-
// Ignore non-JSON
|
|
864
|
+
catch (_a) {
|
|
865
|
+
// Ignore non-JSON
|
|
865
866
|
}
|
|
866
867
|
};
|
|
867
|
-
|
|
868
|
-
this.
|
|
868
|
+
socket.onerror = () => {
|
|
869
|
+
this.ngZone.run(() => {
|
|
870
|
+
if (this.ws === socket && socket.readyState !== WebSocket.CLOSED) {
|
|
871
|
+
socket.close();
|
|
872
|
+
}
|
|
873
|
+
});
|
|
869
874
|
};
|
|
870
|
-
|
|
871
|
-
this.ws
|
|
875
|
+
socket.onclose = () => {
|
|
876
|
+
if (this.ws === socket) {
|
|
877
|
+
this.ws = null;
|
|
878
|
+
}
|
|
879
|
+
const client = this.closeInitiatedByClient;
|
|
880
|
+
this.closeInitiatedByClient = false;
|
|
881
|
+
if (!client) {
|
|
882
|
+
this.ngZone.run(() => this.remoteCloseSubject.next());
|
|
883
|
+
}
|
|
872
884
|
};
|
|
873
885
|
}
|
|
874
886
|
catch (err) {
|
|
@@ -877,344 +889,92 @@ class WebSocketVoiceClientService {
|
|
|
877
889
|
throw err;
|
|
878
890
|
}
|
|
879
891
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
892
|
+
handleJsonMessage(msg) {
|
|
893
|
+
const type = msg.type;
|
|
894
|
+
const typeStr = typeof type === 'string' ? type : '';
|
|
895
|
+
if (typeStr === 'session_ready' || typeStr === 'connected' || typeStr === 'voice_session_started') {
|
|
896
|
+
return;
|
|
885
897
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
/** True when bot (remote participant) is the active speaker. */
|
|
927
|
-
this.speaking$ = this.speakingSubject.asObservable();
|
|
928
|
-
/** True when user (local participant) is the active speaker. */
|
|
929
|
-
this.userSpeaking$ = this.userSpeakingSubject.asObservable();
|
|
930
|
-
/** True when mic is muted. */
|
|
931
|
-
this.micMuted$ = this.micMutedSubject.asObservable();
|
|
932
|
-
/** Emits local mic stream for waveform visualization. */
|
|
933
|
-
this.localStream$ = this.localStreamSubject.asObservable();
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Connect to Daily room. Acquires mic first for waveform, then joins with audio.
|
|
937
|
-
* @param roomUrl Daily room URL (from room_created)
|
|
938
|
-
* @param token Optional meeting token
|
|
939
|
-
*/
|
|
940
|
-
connect(roomUrl, token) {
|
|
941
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
942
|
-
if (this.callObject) {
|
|
943
|
-
yield this.disconnect();
|
|
898
|
+
if (typeStr === 'assistant_speaking' ||
|
|
899
|
+
typeStr === 'bot_speaking') {
|
|
900
|
+
if (msg.active === true || msg.speaking === true) {
|
|
901
|
+
this.assistantSpeakingSubject.next(true);
|
|
944
902
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
948
|
-
const audioTrack = stream.getAudioTracks()[0];
|
|
949
|
-
if (!audioTrack) {
|
|
950
|
-
stream.getTracks().forEach((t) => t.stop());
|
|
951
|
-
throw new Error('No audio track');
|
|
952
|
-
}
|
|
953
|
-
this.localStream = stream;
|
|
954
|
-
this.localStreamSubject.next(stream);
|
|
955
|
-
// Create audio-only call object
|
|
956
|
-
// videoSource: false = no camera, audioSource = our mic track
|
|
957
|
-
const callObject = Daily.createCallObject({
|
|
958
|
-
videoSource: false,
|
|
959
|
-
audioSource: audioTrack,
|
|
960
|
-
});
|
|
961
|
-
this.callObject = callObject;
|
|
962
|
-
this.setupEventHandlers(callObject);
|
|
963
|
-
// Join room; Daily handles playback of remote (bot) audio automatically.
|
|
964
|
-
// Only pass token when it's a non-empty string (Daily rejects undefined/non-string).
|
|
965
|
-
const joinOptions = { url: roomUrl };
|
|
966
|
-
if (typeof token === 'string' && token.trim() !== '') {
|
|
967
|
-
joinOptions.token = token;
|
|
968
|
-
}
|
|
969
|
-
yield callObject.join(joinOptions);
|
|
970
|
-
console.log(`[VoiceDebug] Room connected (Daily join complete) — ${new Date().toISOString()}`);
|
|
971
|
-
const participants = callObject.participants();
|
|
972
|
-
if (participants === null || participants === void 0 ? void 0 : participants.local) {
|
|
973
|
-
this.localSessionId = participants.local.session_id;
|
|
974
|
-
}
|
|
975
|
-
// Initial mute state: Daily starts with audio on
|
|
976
|
-
this.micMutedSubject.next(!callObject.localAudio());
|
|
903
|
+
else if (msg.active === false || msg.speaking === false) {
|
|
904
|
+
this.assistantSpeakingSubject.next(false);
|
|
977
905
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (typeStr === 'user_speaking') {
|
|
909
|
+
if (msg.active === true || msg.speaking === true) {
|
|
910
|
+
this.serverUserSpeakingSubject.next(true);
|
|
981
911
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
setupEventHandlers(call) {
|
|
985
|
-
// active-speaker-change: used ONLY for user speaking detection.
|
|
986
|
-
// Bot speaking is detected by our own AnalyserNode (instant, no debounce).
|
|
987
|
-
call.on('active-speaker-change', (event) => {
|
|
988
|
-
this.ngZone.run(() => {
|
|
989
|
-
var _a;
|
|
990
|
-
const peerId = (_a = event === null || event === void 0 ? void 0 : event.activeSpeaker) === null || _a === void 0 ? void 0 : _a.peerId;
|
|
991
|
-
if (!peerId || !this.localSessionId) {
|
|
992
|
-
this.userSpeakingSubject.next(false);
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
const isLocal = peerId === this.localSessionId;
|
|
996
|
-
this.userSpeakingSubject.next(isLocal);
|
|
997
|
-
});
|
|
998
|
-
});
|
|
999
|
-
// track-started / track-stopped: set up remote audio playback + AnalyserNode monitor.
|
|
1000
|
-
call.on('track-started', (event) => {
|
|
1001
|
-
this.ngZone.run(() => {
|
|
1002
|
-
var _a, _b, _c, _d;
|
|
1003
|
-
const p = event === null || event === void 0 ? void 0 : event.participant;
|
|
1004
|
-
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;
|
|
1005
|
-
const track = event === null || event === void 0 ? void 0 : event.track;
|
|
1006
|
-
if (p && !p.local && type === 'audio') {
|
|
1007
|
-
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()}`);
|
|
1008
|
-
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;
|
|
1009
|
-
if (audioTrack && typeof audioTrack === 'object') {
|
|
1010
|
-
this.playRemoteTrack(audioTrack);
|
|
1011
|
-
this.monitorRemoteAudio(audioTrack);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
});
|
|
1015
|
-
});
|
|
1016
|
-
call.on('track-stopped', (event) => {
|
|
1017
|
-
this.ngZone.run(() => {
|
|
1018
|
-
var _a, _b;
|
|
1019
|
-
const p = event === null || event === void 0 ? void 0 : event.participant;
|
|
1020
|
-
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;
|
|
1021
|
-
if (p && !p.local && type === 'audio') {
|
|
1022
|
-
this.stopRemoteAudioMonitor();
|
|
1023
|
-
this.stopRemoteAudio();
|
|
1024
|
-
}
|
|
1025
|
-
});
|
|
1026
|
-
});
|
|
1027
|
-
call.on('left-meeting', () => {
|
|
1028
|
-
this.ngZone.run(() => this.cleanup());
|
|
1029
|
-
});
|
|
1030
|
-
call.on('error', (event) => {
|
|
1031
|
-
this.ngZone.run(() => {
|
|
1032
|
-
var _a;
|
|
1033
|
-
console.error('DailyVoiceClient: Daily error', (_a = event === null || event === void 0 ? void 0 : event.errorMsg) !== null && _a !== void 0 ? _a : event);
|
|
1034
|
-
this.cleanup();
|
|
1035
|
-
});
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
/**
|
|
1039
|
-
* Play remote (bot) audio track via a dedicated audio element.
|
|
1040
|
-
* Required in many browsers where Daily's internal playback does not output to speakers.
|
|
1041
|
-
*/
|
|
1042
|
-
playRemoteTrack(track) {
|
|
1043
|
-
this.stopRemoteAudio();
|
|
1044
|
-
try {
|
|
1045
|
-
console.log(`[VoiceDebug] playRemoteTrack called — track.readyState=${track.readyState}, track.muted=${track.muted} — ${new Date().toISOString()}`);
|
|
1046
|
-
track.onunmute = () => {
|
|
1047
|
-
console.log(`[VoiceDebug] Remote audio track UNMUTED (audio data arriving) — ${new Date().toISOString()}`);
|
|
1048
|
-
};
|
|
1049
|
-
const stream = new MediaStream([track]);
|
|
1050
|
-
const audio = new Audio();
|
|
1051
|
-
audio.autoplay = true;
|
|
1052
|
-
audio.srcObject = stream;
|
|
1053
|
-
this.remoteAudioElement = audio;
|
|
1054
|
-
audio.onplaying = () => {
|
|
1055
|
-
console.log(`[VoiceDebug] Audio element PLAYING (browser started playback) — ${new Date().toISOString()}`);
|
|
1056
|
-
};
|
|
1057
|
-
let firstTimeUpdate = true;
|
|
1058
|
-
audio.ontimeupdate = () => {
|
|
1059
|
-
if (firstTimeUpdate) {
|
|
1060
|
-
firstTimeUpdate = false;
|
|
1061
|
-
console.log(`[VoiceDebug] Audio element first TIMEUPDATE (actual audio output) — ${new Date().toISOString()}`);
|
|
1062
|
-
}
|
|
1063
|
-
};
|
|
1064
|
-
const p = audio.play();
|
|
1065
|
-
if (p && typeof p.then === 'function') {
|
|
1066
|
-
p.then(() => {
|
|
1067
|
-
console.log(`[VoiceDebug] audio.play() resolved — ${new Date().toISOString()}`);
|
|
1068
|
-
}).catch((err) => {
|
|
1069
|
-
console.warn('DailyVoiceClient: remote audio play failed (may need user gesture)', err);
|
|
1070
|
-
});
|
|
912
|
+
else if (msg.active === false || msg.speaking === false) {
|
|
913
|
+
this.serverUserSpeakingSubject.next(false);
|
|
1071
914
|
}
|
|
915
|
+
return;
|
|
1072
916
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
917
|
+
if (typeStr === 'input_audio_buffer.speech_started') {
|
|
918
|
+
this.serverUserSpeakingSubject.next(true);
|
|
919
|
+
return;
|
|
1075
920
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
* Polls at ~60fps and flips speakingSubject based on actual audio energy.
|
|
1080
|
-
*/
|
|
1081
|
-
monitorRemoteAudio(track) {
|
|
1082
|
-
this.stopRemoteAudioMonitor();
|
|
1083
|
-
try {
|
|
1084
|
-
const ctx = new AudioContext();
|
|
1085
|
-
const source = ctx.createMediaStreamSource(new MediaStream([track]));
|
|
1086
|
-
const analyser = ctx.createAnalyser();
|
|
1087
|
-
analyser.fftSize = 256;
|
|
1088
|
-
source.connect(analyser);
|
|
1089
|
-
this.remoteAudioContext = ctx;
|
|
1090
|
-
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
1091
|
-
const THRESHOLD = 5;
|
|
1092
|
-
const SILENCE_MS = 1500;
|
|
1093
|
-
let lastSoundTime = 0;
|
|
1094
|
-
let isSpeaking = false;
|
|
1095
|
-
const poll = () => {
|
|
1096
|
-
if (!this.remoteAudioContext)
|
|
1097
|
-
return;
|
|
1098
|
-
analyser.getByteFrequencyData(dataArray);
|
|
1099
|
-
let sum = 0;
|
|
1100
|
-
for (let i = 0; i < dataArray.length; i++) {
|
|
1101
|
-
sum += dataArray[i];
|
|
1102
|
-
}
|
|
1103
|
-
const avg = sum / dataArray.length;
|
|
1104
|
-
const now = Date.now();
|
|
1105
|
-
if (avg > THRESHOLD) {
|
|
1106
|
-
lastSoundTime = now;
|
|
1107
|
-
if (!isSpeaking) {
|
|
1108
|
-
isSpeaking = true;
|
|
1109
|
-
console.log(`[VoiceDebug] Bot audio energy detected (speaking=true) — avg=${avg.toFixed(1)} — ${new Date().toISOString()}`);
|
|
1110
|
-
this.ngZone.run(() => {
|
|
1111
|
-
this.userSpeakingSubject.next(false);
|
|
1112
|
-
this.speakingSubject.next(true);
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
else if (isSpeaking && now - lastSoundTime > SILENCE_MS) {
|
|
1117
|
-
isSpeaking = false;
|
|
1118
|
-
console.log(`[VoiceDebug] Bot audio silence detected (speaking=false) — ${new Date().toISOString()}`);
|
|
1119
|
-
this.ngZone.run(() => this.speakingSubject.next(false));
|
|
1120
|
-
}
|
|
1121
|
-
this.remoteSpeakingRAF = requestAnimationFrame(poll);
|
|
1122
|
-
};
|
|
1123
|
-
this.remoteSpeakingRAF = requestAnimationFrame(poll);
|
|
921
|
+
if (typeStr === 'input_audio_buffer.speech_stopped') {
|
|
922
|
+
this.serverUserSpeakingSubject.next(false);
|
|
923
|
+
return;
|
|
1124
924
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
925
|
+
if (typeStr === 'response.audio.delta') {
|
|
926
|
+
this.assistantSpeakingSubject.next(true);
|
|
927
|
+
return;
|
|
1127
928
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
this.remoteSpeakingRAF = null;
|
|
929
|
+
if (typeStr === 'response.audio.done' ||
|
|
930
|
+
typeStr === 'response.output_audio.done') {
|
|
931
|
+
this.assistantSpeakingSubject.next(false);
|
|
932
|
+
return;
|
|
1133
933
|
}
|
|
1134
|
-
if (
|
|
1135
|
-
this.
|
|
1136
|
-
|
|
934
|
+
if (typeStr === 'user_transcript' && typeof msg.text === 'string') {
|
|
935
|
+
this.userTranscriptSubject.next({
|
|
936
|
+
text: msg.text,
|
|
937
|
+
final: msg.final === true,
|
|
938
|
+
});
|
|
939
|
+
return;
|
|
1137
940
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if (this.remoteAudioElement) {
|
|
1141
|
-
try {
|
|
1142
|
-
this.remoteAudioElement.pause();
|
|
1143
|
-
this.remoteAudioElement.srcObject = null;
|
|
1144
|
-
}
|
|
1145
|
-
catch (_) { }
|
|
1146
|
-
this.remoteAudioElement = null;
|
|
941
|
+
if (typeStr === 'bot_transcript' && typeof msg.text === 'string') {
|
|
942
|
+
this.botTranscriptSubject.next(msg.text);
|
|
1147
943
|
}
|
|
1148
944
|
}
|
|
1149
|
-
/** Set mic muted state. */
|
|
1150
|
-
setMuted(muted) {
|
|
1151
|
-
if (!this.callObject)
|
|
1152
|
-
return;
|
|
1153
|
-
this.callObject.setLocalAudio(!muted);
|
|
1154
|
-
this.micMutedSubject.next(muted);
|
|
1155
|
-
}
|
|
1156
|
-
/** Disconnect and cleanup. */
|
|
1157
945
|
disconnect() {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
this.cleanup();
|
|
1161
|
-
return;
|
|
1162
|
-
}
|
|
1163
|
-
try {
|
|
1164
|
-
yield this.callObject.leave();
|
|
1165
|
-
}
|
|
1166
|
-
catch (e) {
|
|
1167
|
-
// ignore
|
|
1168
|
-
}
|
|
1169
|
-
this.cleanup();
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
|
-
cleanup() {
|
|
1173
|
-
this.stopRemoteAudioMonitor();
|
|
1174
|
-
this.stopRemoteAudio();
|
|
1175
|
-
if (this.callObject) {
|
|
1176
|
-
this.callObject.destroy().catch(() => { });
|
|
1177
|
-
this.callObject = null;
|
|
1178
|
-
}
|
|
1179
|
-
if (this.localStream) {
|
|
1180
|
-
this.localStream.getTracks().forEach((t) => t.stop());
|
|
1181
|
-
this.localStream = null;
|
|
946
|
+
if (!this.ws) {
|
|
947
|
+
return;
|
|
1182
948
|
}
|
|
1183
|
-
this.
|
|
1184
|
-
this.
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
949
|
+
this.closeInitiatedByClient = true;
|
|
950
|
+
this.ws.close();
|
|
951
|
+
}
|
|
952
|
+
get isConnected() {
|
|
953
|
+
var _a;
|
|
954
|
+
return ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN;
|
|
1188
955
|
}
|
|
1189
956
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
957
|
+
WebSocketVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function WebSocketVoiceClientService_Factory() { return new WebSocketVoiceClientService(i0.ɵɵinject(i0.NgZone)); }, token: WebSocketVoiceClientService, providedIn: "root" });
|
|
958
|
+
WebSocketVoiceClientService.decorators = [
|
|
1192
959
|
{ type: Injectable, args: [{
|
|
1193
960
|
providedIn: 'root',
|
|
1194
961
|
},] }
|
|
1195
962
|
];
|
|
1196
|
-
|
|
963
|
+
WebSocketVoiceClientService.ctorParameters = () => [
|
|
1197
964
|
{ type: NgZone }
|
|
1198
965
|
];
|
|
1199
966
|
|
|
1200
967
|
/**
|
|
1201
|
-
* Voice agent orchestrator
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
1204
|
-
* - Native WebSocket (WebSocketVoiceClientService) for signaling (room_created, transcripts)
|
|
1205
|
-
* - Daily.js (DailyVoiceClientService) for WebRTC audio. Audio does NOT flow over WebSocket.
|
|
1206
|
-
*
|
|
1207
|
-
* - Maintains callState, statusText, duration, isMicMuted, isUserSpeaking, audioLevels
|
|
1208
|
-
* - Uses WebSocket for room_created and transcripts only (no audio)
|
|
1209
|
-
* - Uses Daily.js for all audio, mic, and real-time speaking detection
|
|
968
|
+
* Voice agent orchestrator: single WebSocket (`ws_url` from POST /ai/ask-voice-socket)
|
|
969
|
+
* for session events, transcripts, and optional speaking hints; local mic for capture
|
|
970
|
+
* and waveform only (no Daily/WebRTC room).
|
|
1210
971
|
*/
|
|
1211
972
|
class VoiceAgentService {
|
|
1212
|
-
constructor(audioAnalyzer, wsClient,
|
|
973
|
+
constructor(audioAnalyzer, wsClient, platformTokenRefresh,
|
|
1213
974
|
/** `Object` not `object` — ngc metadata collection rejects the `object` type in DI params. */
|
|
1214
975
|
platformId) {
|
|
1215
976
|
this.audioAnalyzer = audioAnalyzer;
|
|
1216
977
|
this.wsClient = wsClient;
|
|
1217
|
-
this.dailyClient = dailyClient;
|
|
1218
978
|
this.platformTokenRefresh = platformTokenRefresh;
|
|
1219
979
|
this.platformId = platformId;
|
|
1220
980
|
this.callStateSubject = new BehaviorSubject('idle');
|
|
@@ -1227,6 +987,8 @@ class VoiceAgentService {
|
|
|
1227
987
|
this.botTranscriptSubject = new Subject();
|
|
1228
988
|
this.callStartTime = 0;
|
|
1229
989
|
this.durationInterval = null;
|
|
990
|
+
this.localMicStream = null;
|
|
991
|
+
this.endCall$ = new Subject();
|
|
1230
992
|
this.subscriptions = new Subscription();
|
|
1231
993
|
this.destroy$ = new Subject();
|
|
1232
994
|
this.callState$ = this.callStateSubject.asObservable();
|
|
@@ -1237,8 +999,10 @@ class VoiceAgentService {
|
|
|
1237
999
|
this.audioLevels$ = this.audioLevelsSubject.asObservable();
|
|
1238
1000
|
this.userTranscript$ = this.userTranscriptSubject.asObservable();
|
|
1239
1001
|
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
1240
|
-
// Waveform visualization only - do NOT use for speaking state
|
|
1241
1002
|
this.subscriptions.add(this.audioAnalyzer.audioLevels$.subscribe((levels) => this.audioLevelsSubject.next(levels)));
|
|
1003
|
+
this.subscriptions.add(this.wsClient.remoteClose$
|
|
1004
|
+
.pipe(takeUntil(this.destroy$))
|
|
1005
|
+
.subscribe(() => void this.handleRemoteClose()));
|
|
1242
1006
|
}
|
|
1243
1007
|
ngOnDestroy() {
|
|
1244
1008
|
this.destroy$.next();
|
|
@@ -1249,11 +1013,12 @@ class VoiceAgentService {
|
|
|
1249
1013
|
resetToIdle() {
|
|
1250
1014
|
if (this.callStateSubject.value === 'idle')
|
|
1251
1015
|
return;
|
|
1016
|
+
this.endCall$.next();
|
|
1252
1017
|
this.stopDurationTimer();
|
|
1018
|
+
this.callStartTime = 0;
|
|
1253
1019
|
this.audioAnalyzer.stop();
|
|
1020
|
+
this.stopLocalMic();
|
|
1254
1021
|
this.wsClient.disconnect();
|
|
1255
|
-
// Fire-and-forget: Daily disconnect is async; connect() will await if needed
|
|
1256
|
-
void this.dailyClient.disconnect();
|
|
1257
1022
|
this.callStateSubject.next('idle');
|
|
1258
1023
|
this.statusTextSubject.next('');
|
|
1259
1024
|
this.durationSubject.next('0:00');
|
|
@@ -1268,9 +1033,6 @@ class VoiceAgentService {
|
|
|
1268
1033
|
this.callStateSubject.next('connecting');
|
|
1269
1034
|
this.statusTextSubject.next('Connecting...');
|
|
1270
1035
|
let accessToken = token;
|
|
1271
|
-
// Align with chat drawer token handling: always delegate to
|
|
1272
|
-
// PlatformTokenRefreshService when we have a usersApiUrl, so it can
|
|
1273
|
-
// fall back to stored tokens even if the caller passed an empty token.
|
|
1274
1036
|
if (usersApiUrl && isPlatformBrowser(this.platformId)) {
|
|
1275
1037
|
try {
|
|
1276
1038
|
const ensured = yield this.platformTokenRefresh
|
|
@@ -1286,7 +1048,7 @@ class VoiceAgentService {
|
|
|
1286
1048
|
}
|
|
1287
1049
|
}
|
|
1288
1050
|
const baseUrl = apiUrl.replace(/\/$/, '');
|
|
1289
|
-
const postUrl = `${baseUrl}/ai/ask-voice`;
|
|
1051
|
+
const postUrl = `${baseUrl}/ai/ask-voice-socket`;
|
|
1290
1052
|
const headers = {
|
|
1291
1053
|
'Content-Type': 'application/json',
|
|
1292
1054
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -1298,7 +1060,6 @@ class VoiceAgentService {
|
|
|
1298
1060
|
eventToken,
|
|
1299
1061
|
'ngrok-skip-browser-warning': 'true',
|
|
1300
1062
|
};
|
|
1301
|
-
// POST to get ws_url for signaling
|
|
1302
1063
|
const res = yield fetch(postUrl, {
|
|
1303
1064
|
method: 'POST',
|
|
1304
1065
|
headers,
|
|
@@ -1312,33 +1073,21 @@ class VoiceAgentService {
|
|
|
1312
1073
|
throw new Error(`HTTP ${res.status}`);
|
|
1313
1074
|
}
|
|
1314
1075
|
const json = yield res.json();
|
|
1315
|
-
const wsUrl = json === null || json === void 0 ? void 0 : json.
|
|
1316
|
-
|
|
1076
|
+
const wsUrl = (typeof (json === null || json === void 0 ? void 0 : json.ws_url) === 'string' && json.ws_url) ||
|
|
1077
|
+
(typeof (json === null || json === void 0 ? void 0 : json.rn_ws_url) === 'string' && json.rn_ws_url);
|
|
1078
|
+
if (!wsUrl) {
|
|
1317
1079
|
throw new Error('No ws_url in response');
|
|
1318
1080
|
}
|
|
1319
|
-
|
|
1320
|
-
this.wsClient.roomCreated$
|
|
1321
|
-
.pipe(take(1), takeUntil(this.destroy$))
|
|
1322
|
-
.subscribe((roomUrl) => __awaiter(this, void 0, void 0, function* () {
|
|
1323
|
-
try {
|
|
1324
|
-
yield this.onRoomCreated(roomUrl);
|
|
1325
|
-
}
|
|
1326
|
-
catch (err) {
|
|
1327
|
-
console.error('Daily join failed:', err);
|
|
1328
|
-
this.callStateSubject.next('ended');
|
|
1329
|
-
this.statusTextSubject.next('Connection failed');
|
|
1330
|
-
yield this.disconnect();
|
|
1331
|
-
throw err;
|
|
1332
|
-
}
|
|
1333
|
-
}));
|
|
1334
|
-
// Forward transcripts from WebSocket
|
|
1081
|
+
const untilCallEnds$ = merge(this.destroy$, this.endCall$);
|
|
1335
1082
|
this.subscriptions.add(this.wsClient.userTranscript$
|
|
1336
|
-
.pipe(takeUntil(
|
|
1083
|
+
.pipe(takeUntil(untilCallEnds$))
|
|
1337
1084
|
.subscribe((t) => this.userTranscriptSubject.next(t)));
|
|
1338
1085
|
this.subscriptions.add(this.wsClient.botTranscript$
|
|
1339
|
-
.pipe(takeUntil(
|
|
1086
|
+
.pipe(takeUntil(untilCallEnds$))
|
|
1340
1087
|
.subscribe((t) => this.botTranscriptSubject.next(t)));
|
|
1341
|
-
|
|
1088
|
+
this.subscriptions.add(this.wsClient.opened$
|
|
1089
|
+
.pipe(takeUntil(untilCallEnds$), take(1))
|
|
1090
|
+
.subscribe(() => void this.onWebsocketOpened()));
|
|
1342
1091
|
this.wsClient.connect(wsUrl);
|
|
1343
1092
|
}
|
|
1344
1093
|
catch (error) {
|
|
@@ -1350,59 +1099,113 @@ class VoiceAgentService {
|
|
|
1350
1099
|
}
|
|
1351
1100
|
});
|
|
1352
1101
|
}
|
|
1353
|
-
|
|
1102
|
+
onWebsocketOpened() {
|
|
1354
1103
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
.
|
|
1360
|
-
.
|
|
1361
|
-
this.
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
this.
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1104
|
+
if (this.callStateSubject.value !== 'connecting') {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
yield this.startLocalMic();
|
|
1109
|
+
this.statusTextSubject.next('Connected');
|
|
1110
|
+
this.callStateSubject.next('connected');
|
|
1111
|
+
this.wireSpeakingState();
|
|
1112
|
+
}
|
|
1113
|
+
catch (err) {
|
|
1114
|
+
console.error('[HiveGpt Voice] Mic or session setup failed', err);
|
|
1115
|
+
this.callStateSubject.next('ended');
|
|
1116
|
+
this.statusTextSubject.next('Microphone unavailable');
|
|
1117
|
+
yield this.disconnect();
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
wireSpeakingState() {
|
|
1122
|
+
const untilCallEnds$ = merge(this.destroy$, this.endCall$);
|
|
1123
|
+
const transcriptDrivenAssistant$ = this.wsClient.botTranscript$.pipe(switchMap(() => concat(of(true), timer(800).pipe(map(() => false)))), distinctUntilChanged());
|
|
1124
|
+
const assistantTalking$ = merge(this.wsClient.assistantSpeaking$, transcriptDrivenAssistant$).pipe(distinctUntilChanged(), startWith(false));
|
|
1125
|
+
const userTalking$ = combineLatest([
|
|
1126
|
+
this.audioAnalyzer.isUserSpeaking$,
|
|
1127
|
+
this.wsClient.serverUserSpeaking$.pipe(startWith(false)),
|
|
1128
|
+
]).pipe(map(([local, server]) => local || server), distinctUntilChanged(), startWith(false));
|
|
1129
|
+
this.subscriptions.add(combineLatest([assistantTalking$, userTalking$])
|
|
1130
|
+
.pipe(takeUntil(untilCallEnds$))
|
|
1131
|
+
.subscribe(([bot, user]) => {
|
|
1132
|
+
const current = this.callStateSubject.value;
|
|
1133
|
+
if (user) {
|
|
1134
|
+
this.isUserSpeakingSubject.next(true);
|
|
1135
|
+
this.callStateSubject.next('listening');
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
this.isUserSpeakingSubject.next(false);
|
|
1139
|
+
}
|
|
1140
|
+
if (user) {
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (bot) {
|
|
1144
|
+
if (this.callStartTime === 0) {
|
|
1373
1145
|
this.callStartTime = Date.now();
|
|
1374
1146
|
this.startDurationTimer();
|
|
1375
|
-
this.callStateSubject.next('talking');
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
if (user) {
|
|
1379
|
-
this.callStateSubject.next('listening');
|
|
1380
|
-
}
|
|
1381
|
-
else if (bot) {
|
|
1382
|
-
this.callStateSubject.next('talking');
|
|
1383
|
-
}
|
|
1384
|
-
else if (current === 'talking' || current === 'listening') {
|
|
1385
|
-
this.callStateSubject.next('connected');
|
|
1386
1147
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1148
|
+
this.callStateSubject.next('talking');
|
|
1149
|
+
}
|
|
1150
|
+
else if (current === 'talking' || current === 'listening') {
|
|
1151
|
+
this.callStateSubject.next('connected');
|
|
1152
|
+
}
|
|
1153
|
+
}));
|
|
1154
|
+
}
|
|
1155
|
+
startLocalMic() {
|
|
1156
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1157
|
+
this.stopLocalMic();
|
|
1158
|
+
const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
1159
|
+
const track = stream.getAudioTracks()[0];
|
|
1160
|
+
if (!track) {
|
|
1161
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
1162
|
+
throw new Error('No audio track');
|
|
1163
|
+
}
|
|
1164
|
+
this.localMicStream = stream;
|
|
1165
|
+
this.isMicMutedSubject.next(!track.enabled);
|
|
1166
|
+
this.audioAnalyzer.start(stream);
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
stopLocalMic() {
|
|
1170
|
+
if (this.localMicStream) {
|
|
1171
|
+
this.localMicStream.getTracks().forEach((t) => t.stop());
|
|
1172
|
+
this.localMicStream = null;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
handleRemoteClose() {
|
|
1176
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1177
|
+
const state = this.callStateSubject.value;
|
|
1178
|
+
if (state === 'idle' || state === 'ended')
|
|
1179
|
+
return;
|
|
1180
|
+
this.endCall$.next();
|
|
1181
|
+
this.stopDurationTimer();
|
|
1182
|
+
this.callStartTime = 0;
|
|
1183
|
+
this.audioAnalyzer.stop();
|
|
1184
|
+
this.stopLocalMic();
|
|
1185
|
+
this.callStateSubject.next('ended');
|
|
1186
|
+
this.statusTextSubject.next('Connection lost');
|
|
1390
1187
|
});
|
|
1391
1188
|
}
|
|
1392
1189
|
disconnect() {
|
|
1393
1190
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1191
|
+
this.endCall$.next();
|
|
1394
1192
|
this.stopDurationTimer();
|
|
1193
|
+
this.callStartTime = 0;
|
|
1395
1194
|
this.audioAnalyzer.stop();
|
|
1396
|
-
|
|
1397
|
-
yield this.dailyClient.disconnect();
|
|
1195
|
+
this.stopLocalMic();
|
|
1398
1196
|
this.wsClient.disconnect();
|
|
1399
1197
|
this.callStateSubject.next('ended');
|
|
1400
1198
|
this.statusTextSubject.next('Call Ended');
|
|
1401
1199
|
});
|
|
1402
1200
|
}
|
|
1403
1201
|
toggleMic() {
|
|
1404
|
-
|
|
1405
|
-
this.
|
|
1202
|
+
var _a;
|
|
1203
|
+
const nextMuted = !this.isMicMutedSubject.value;
|
|
1204
|
+
const track = (_a = this.localMicStream) === null || _a === void 0 ? void 0 : _a.getAudioTracks()[0];
|
|
1205
|
+
if (track) {
|
|
1206
|
+
track.enabled = !nextMuted;
|
|
1207
|
+
}
|
|
1208
|
+
this.isMicMutedSubject.next(nextMuted);
|
|
1406
1209
|
}
|
|
1407
1210
|
startDurationTimer() {
|
|
1408
1211
|
const updateDuration = () => {
|
|
@@ -1423,7 +1226,7 @@ class VoiceAgentService {
|
|
|
1423
1226
|
}
|
|
1424
1227
|
}
|
|
1425
1228
|
}
|
|
1426
|
-
VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService), i0.ɵɵinject(WebSocketVoiceClientService), i0.ɵɵinject(
|
|
1229
|
+
VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService), i0.ɵɵinject(WebSocketVoiceClientService), i0.ɵɵinject(PlatformTokenRefreshService), i0.ɵɵinject(i0.PLATFORM_ID)); }, token: VoiceAgentService, providedIn: "root" });
|
|
1427
1230
|
VoiceAgentService.decorators = [
|
|
1428
1231
|
{ type: Injectable, args: [{
|
|
1429
1232
|
providedIn: 'root',
|
|
@@ -1432,7 +1235,6 @@ VoiceAgentService.decorators = [
|
|
|
1432
1235
|
VoiceAgentService.ctorParameters = () => [
|
|
1433
1236
|
{ type: AudioAnalyzerService },
|
|
1434
1237
|
{ type: WebSocketVoiceClientService },
|
|
1435
|
-
{ type: DailyVoiceClientService },
|
|
1436
1238
|
{ type: PlatformTokenRefreshService },
|
|
1437
1239
|
{ type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }
|
|
1438
1240
|
];
|
|
@@ -1548,10 +1350,21 @@ class VoiceAgentModalComponent {
|
|
|
1548
1350
|
toggleMic() {
|
|
1549
1351
|
this.voiceAgentService.toggleMic();
|
|
1550
1352
|
}
|
|
1551
|
-
/**
|
|
1552
|
-
|
|
1353
|
+
/**
|
|
1354
|
+
* Map audio level (0–100) to waveform bar height in px with a gaussian
|
|
1355
|
+
* bell-curve envelope so centre bars are tallest and edge bars appear
|
|
1356
|
+
* as tiny dots — matching the audio-waveform reference design.
|
|
1357
|
+
*/
|
|
1358
|
+
getWaveformHeight(level, index) {
|
|
1553
1359
|
const n = Math.min(100, Math.max(0, level !== null && level !== void 0 ? level : 0));
|
|
1554
|
-
|
|
1360
|
+
const total = this.audioLevels.length || 60;
|
|
1361
|
+
const center = (total - 1) / 2;
|
|
1362
|
+
const sigma = total / 5;
|
|
1363
|
+
const envelope = Math.exp(-Math.pow(index - center, 2) / (2 * sigma * sigma));
|
|
1364
|
+
// Minimum height scales with envelope so edges show as dots even in silence
|
|
1365
|
+
const minHeight = 2 + envelope * 3;
|
|
1366
|
+
const maxHeight = 4 + envelope * 38;
|
|
1367
|
+
return minHeight + (n / 100) * (maxHeight - minHeight);
|
|
1555
1368
|
}
|
|
1556
1369
|
/** Status label for active call. */
|
|
1557
1370
|
get statusLabel() {
|
|
@@ -1589,8 +1402,8 @@ class VoiceAgentModalComponent {
|
|
|
1589
1402
|
VoiceAgentModalComponent.decorators = [
|
|
1590
1403
|
{ type: Component, args: [{
|
|
1591
1404
|
selector: 'hivegpt-voice-agent-modal',
|
|
1592
|
-
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' && isUserSpeaking\"\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 [style.height.px]=\"getWaveformHeight(level)\"\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",
|
|
1593
|
-
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
|
|
1405
|
+
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",
|
|
1406
|
+
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}"]
|
|
1594
1407
|
},] }
|
|
1595
1408
|
];
|
|
1596
1409
|
VoiceAgentModalComponent.ctorParameters = () => [
|
|
@@ -5461,7 +5274,7 @@ ChatBotComponent.propDecorators = {
|
|
|
5461
5274
|
};
|
|
5462
5275
|
|
|
5463
5276
|
/**
|
|
5464
|
-
* Voice agent module. Uses native WebSocket
|
|
5277
|
+
* Voice agent module. Uses native WebSocket for the voice session.
|
|
5465
5278
|
* Does NOT use Socket.IO or ngx-socket-io.
|
|
5466
5279
|
*/
|
|
5467
5280
|
class VoiceAgentModule {
|
|
@@ -5477,8 +5290,7 @@ VoiceAgentModule.decorators = [
|
|
|
5477
5290
|
providers: [
|
|
5478
5291
|
VoiceAgentService,
|
|
5479
5292
|
AudioAnalyzerService,
|
|
5480
|
-
WebSocketVoiceClientService
|
|
5481
|
-
DailyVoiceClientService
|
|
5293
|
+
WebSocketVoiceClientService
|
|
5482
5294
|
],
|
|
5483
5295
|
exports: [
|
|
5484
5296
|
VoiceAgentModalComponent
|
|
@@ -5749,5 +5561,5 @@ HiveGptModule.decorators = [
|
|
|
5749
5561
|
* Generated bundle index. Do not edit.
|
|
5750
5562
|
*/
|
|
5751
5563
|
|
|
5752
|
-
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,
|
|
5564
|
+
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, VideoPlayerComponent as ɵg, SafeHtmlPipe as ɵh, BotHtmlEditorComponent as ɵi };
|
|
5753
5565
|
//# sourceMappingURL=hivegpt-hiveai-angular.js.map
|