@hivegpt/hiveai-angular 0.0.583 → 0.0.585
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 +239 -547
- 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 -24
- package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +174 -236
- package/esm2015/lib/components/voice-agent/voice-agent.module.js +3 -5
- package/fesm2015/hivegpt-hiveai-angular.js +190 -460
- 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 +7 -4
- package/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts.map +1 -1
- package/lib/components/voice-agent/services/voice-agent.service.d.ts +24 -26
- package/lib/components/voice-agent/services/voice-agent.service.d.ts.map +1 -1
- 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
- package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +0 -206
- package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts +0 -59
- package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts.map +0 -1
|
@@ -5,14 +5,16 @@ 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
|
|
9
|
-
import { switchMap, catchError, filter, take, map,
|
|
8
|
+
import { BehaviorSubject, of, throwError, Subject, Subscription } from 'rxjs';
|
|
9
|
+
import { switchMap, catchError, filter, take, map, 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 { PipecatClient, RTVIEvent } from '@pipecat-ai/client-js';
|
|
17
|
+
import { WebSocketTransport } from '@pipecat-ai/websocket-transport';
|
|
16
18
|
import { MatIconModule } from '@angular/material/icon';
|
|
17
19
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
18
20
|
import { QuillModule } from 'ngx-quill';
|
|
@@ -805,219 +807,23 @@ AudioAnalyzerService.decorators = [
|
|
|
805
807
|
];
|
|
806
808
|
|
|
807
809
|
/**
|
|
808
|
-
*
|
|
809
|
-
* CRITICAL: Uses native WebSocket only. NO Socket.IO, NO ngx-socket-io.
|
|
810
|
+
* Voice agent orchestrator using the official PipecatClient SDK.
|
|
810
811
|
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
this.userTranscriptSubject = new Subject();
|
|
823
|
-
this.botTranscriptSubject = new Subject();
|
|
824
|
-
this.assistantSpeakingSubject = new Subject();
|
|
825
|
-
this.serverUserSpeakingSubject = new Subject();
|
|
826
|
-
this.audioChunkSubject = new Subject();
|
|
827
|
-
/** Fires once each time the WebSocket reaches OPEN. */
|
|
828
|
-
this.opened$ = this.openedSubject.asObservable();
|
|
829
|
-
/** Fires when the socket closes without a client-initiated {@link disconnect}. */
|
|
830
|
-
this.remoteClose$ = this.remoteCloseSubject.asObservable();
|
|
831
|
-
this.userTranscript$ = this.userTranscriptSubject.asObservable();
|
|
832
|
-
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
833
|
-
/** Assistant/bot speaking, when the server sends explicit events (see {@link handleJsonMessage}). */
|
|
834
|
-
this.assistantSpeaking$ = this.assistantSpeakingSubject.asObservable();
|
|
835
|
-
/** User speaking from server-side VAD, if provided. */
|
|
836
|
-
this.serverUserSpeaking$ = this.serverUserSpeakingSubject.asObservable();
|
|
837
|
-
/** Binary audio frames from server (when backend streams bot audio over WS). */
|
|
838
|
-
this.audioChunk$ = this.audioChunkSubject.asObservable();
|
|
839
|
-
}
|
|
840
|
-
connect(wsUrl) {
|
|
841
|
-
var _a;
|
|
842
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
if (this.ws) {
|
|
846
|
-
this.closeInitiatedByClient = true;
|
|
847
|
-
this.ws.close();
|
|
848
|
-
}
|
|
849
|
-
try {
|
|
850
|
-
const socket = new WebSocket(wsUrl);
|
|
851
|
-
this.ws = socket;
|
|
852
|
-
socket.onopen = () => {
|
|
853
|
-
if (this.ws !== socket)
|
|
854
|
-
return;
|
|
855
|
-
this.ngZone.run(() => this.openedSubject.next());
|
|
856
|
-
};
|
|
857
|
-
socket.onmessage = (event) => {
|
|
858
|
-
if (this.ws !== socket)
|
|
859
|
-
return;
|
|
860
|
-
void this.handleIncomingMessage(event.data);
|
|
861
|
-
};
|
|
862
|
-
socket.onerror = () => {
|
|
863
|
-
this.ngZone.run(() => {
|
|
864
|
-
if (this.ws === socket && socket.readyState !== WebSocket.CLOSED) {
|
|
865
|
-
socket.close();
|
|
866
|
-
}
|
|
867
|
-
});
|
|
868
|
-
};
|
|
869
|
-
socket.onclose = () => {
|
|
870
|
-
if (this.ws === socket) {
|
|
871
|
-
this.ws = null;
|
|
872
|
-
}
|
|
873
|
-
const client = this.closeInitiatedByClient;
|
|
874
|
-
this.closeInitiatedByClient = false;
|
|
875
|
-
if (!client) {
|
|
876
|
-
this.ngZone.run(() => this.remoteCloseSubject.next());
|
|
877
|
-
}
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
catch (err) {
|
|
881
|
-
console.error('WebSocketVoiceClient: connect failed', err);
|
|
882
|
-
this.ws = null;
|
|
883
|
-
throw err;
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
handleIncomingMessage(payload) {
|
|
887
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
888
|
-
if (typeof payload === 'string') {
|
|
889
|
-
this.handleJsonString(payload);
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
if (payload instanceof ArrayBuffer) {
|
|
893
|
-
this.handleBinaryMessage(payload);
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
if (payload instanceof Blob) {
|
|
897
|
-
const ab = yield payload.arrayBuffer();
|
|
898
|
-
this.handleBinaryMessage(ab);
|
|
899
|
-
}
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
handleJsonString(jsonText) {
|
|
903
|
-
try {
|
|
904
|
-
const msg = JSON.parse(jsonText);
|
|
905
|
-
this.ngZone.run(() => this.handleJsonMessage(msg));
|
|
906
|
-
}
|
|
907
|
-
catch (_a) {
|
|
908
|
-
// Ignore non-JSON
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
handleBinaryMessage(buffer) {
|
|
912
|
-
// Some backends wrap JSON events inside binary WS frames.
|
|
913
|
-
const maybeText = this.tryDecodeUtf8(buffer);
|
|
914
|
-
if (maybeText !== null) {
|
|
915
|
-
this.handleJsonString(maybeText);
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
// Otherwise treat binary as streamed assistant audio.
|
|
919
|
-
this.ngZone.run(() => this.audioChunkSubject.next(buffer));
|
|
920
|
-
}
|
|
921
|
-
tryDecodeUtf8(buffer) {
|
|
922
|
-
try {
|
|
923
|
-
const text = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
924
|
-
const trimmed = text.trim();
|
|
925
|
-
if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) {
|
|
926
|
-
return null;
|
|
927
|
-
}
|
|
928
|
-
return trimmed;
|
|
929
|
-
}
|
|
930
|
-
catch (_a) {
|
|
931
|
-
return null;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
handleJsonMessage(msg) {
|
|
935
|
-
const type = msg.type;
|
|
936
|
-
const typeStr = typeof type === 'string' ? type : '';
|
|
937
|
-
if (typeStr === 'session_ready' || typeStr === 'connected' || typeStr === 'voice_session_started') {
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
if (typeStr === 'assistant_speaking' ||
|
|
941
|
-
typeStr === 'bot_speaking') {
|
|
942
|
-
if (msg.active === true || msg.speaking === true) {
|
|
943
|
-
this.assistantSpeakingSubject.next(true);
|
|
944
|
-
}
|
|
945
|
-
else if (msg.active === false || msg.speaking === false) {
|
|
946
|
-
this.assistantSpeakingSubject.next(false);
|
|
947
|
-
}
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
if (typeStr === 'user_speaking') {
|
|
951
|
-
if (msg.active === true || msg.speaking === true) {
|
|
952
|
-
this.serverUserSpeakingSubject.next(true);
|
|
953
|
-
}
|
|
954
|
-
else if (msg.active === false || msg.speaking === false) {
|
|
955
|
-
this.serverUserSpeakingSubject.next(false);
|
|
956
|
-
}
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
if (typeStr === 'input_audio_buffer.speech_started') {
|
|
960
|
-
this.serverUserSpeakingSubject.next(true);
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
if (typeStr === 'input_audio_buffer.speech_stopped') {
|
|
964
|
-
this.serverUserSpeakingSubject.next(false);
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
if (typeStr === 'response.audio.delta') {
|
|
968
|
-
this.assistantSpeakingSubject.next(true);
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
if (typeStr === 'response.audio.done' ||
|
|
972
|
-
typeStr === 'response.output_audio.done') {
|
|
973
|
-
this.assistantSpeakingSubject.next(false);
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
if (typeStr === 'user_transcript' && typeof msg.text === 'string') {
|
|
977
|
-
this.userTranscriptSubject.next({
|
|
978
|
-
text: msg.text,
|
|
979
|
-
final: msg.final === true,
|
|
980
|
-
});
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
if (typeStr === 'bot_transcript' && typeof msg.text === 'string') {
|
|
984
|
-
this.botTranscriptSubject.next(msg.text);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
disconnect() {
|
|
988
|
-
if (!this.ws) {
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
this.closeInitiatedByClient = true;
|
|
992
|
-
this.ws.close();
|
|
993
|
-
}
|
|
994
|
-
get isConnected() {
|
|
995
|
-
var _a;
|
|
996
|
-
return ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
WebSocketVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function WebSocketVoiceClientService_Factory() { return new WebSocketVoiceClientService(i0.ɵɵinject(i0.NgZone)); }, token: WebSocketVoiceClientService, providedIn: "root" });
|
|
1000
|
-
WebSocketVoiceClientService.decorators = [
|
|
1001
|
-
{ type: Injectable, args: [{
|
|
1002
|
-
providedIn: 'root',
|
|
1003
|
-
},] }
|
|
1004
|
-
];
|
|
1005
|
-
WebSocketVoiceClientService.ctorParameters = () => [
|
|
1006
|
-
{ type: NgZone }
|
|
1007
|
-
];
|
|
1008
|
-
|
|
1009
|
-
/**
|
|
1010
|
-
* Voice agent orchestrator: single WebSocket (`ws_url` from POST /ai/ask-voice-socket)
|
|
1011
|
-
* for session events, transcripts, and optional speaking hints; local mic for capture
|
|
1012
|
-
* and waveform only (no Daily/WebRTC room).
|
|
812
|
+
* Audio flow (mirrors the React reference implementation):
|
|
813
|
+
* - Local mic: acquired by PipecatClient.initDevices(); local track fed to
|
|
814
|
+
* AudioAnalyzerService for waveform visualisation.
|
|
815
|
+
* - Bot audio: received as a MediaStreamTrack via RTVIEvent.TrackStarted,
|
|
816
|
+
* played through a hidden <audio> element.
|
|
817
|
+
* - All binary protobuf framing / RTVI protocol handled by
|
|
818
|
+
* @pipecat-ai/client-js + @pipecat-ai/websocket-transport.
|
|
1013
819
|
*/
|
|
1014
820
|
class VoiceAgentService {
|
|
1015
|
-
constructor(audioAnalyzer,
|
|
821
|
+
constructor(audioAnalyzer, platformTokenRefresh, ngZone,
|
|
1016
822
|
/** `Object` not `object` — ngc metadata collection rejects the `object` type in DI params. */
|
|
1017
823
|
platformId) {
|
|
1018
824
|
this.audioAnalyzer = audioAnalyzer;
|
|
1019
|
-
this.wsClient = wsClient;
|
|
1020
825
|
this.platformTokenRefresh = platformTokenRefresh;
|
|
826
|
+
this.ngZone = ngZone;
|
|
1021
827
|
this.platformId = platformId;
|
|
1022
828
|
this.callStateSubject = new BehaviorSubject('idle');
|
|
1023
829
|
this.statusTextSubject = new BehaviorSubject('');
|
|
@@ -1029,11 +835,8 @@ class VoiceAgentService {
|
|
|
1029
835
|
this.botTranscriptSubject = new Subject();
|
|
1030
836
|
this.callStartTime = 0;
|
|
1031
837
|
this.durationInterval = null;
|
|
1032
|
-
this.
|
|
1033
|
-
this.
|
|
1034
|
-
this.pendingRemoteAudio = [];
|
|
1035
|
-
this.remoteAudioPlaying = false;
|
|
1036
|
-
this.endCall$ = new Subject();
|
|
838
|
+
this.pcClient = null;
|
|
839
|
+
this.botAudioElement = null;
|
|
1037
840
|
this.subscriptions = new Subscription();
|
|
1038
841
|
this.destroy$ = new Subject();
|
|
1039
842
|
this.callState$ = this.callStateSubject.asObservable();
|
|
@@ -1045,29 +848,17 @@ class VoiceAgentService {
|
|
|
1045
848
|
this.userTranscript$ = this.userTranscriptSubject.asObservable();
|
|
1046
849
|
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
1047
850
|
this.subscriptions.add(this.audioAnalyzer.audioLevels$.subscribe((levels) => this.audioLevelsSubject.next(levels)));
|
|
1048
|
-
this.subscriptions.add(this.wsClient.remoteClose$
|
|
1049
|
-
.pipe(takeUntil(this.destroy$))
|
|
1050
|
-
.subscribe(() => void this.handleRemoteClose()));
|
|
1051
|
-
this.subscriptions.add(this.wsClient.audioChunk$
|
|
1052
|
-
.pipe(takeUntil(this.destroy$))
|
|
1053
|
-
.subscribe((chunk) => this.enqueueRemoteAudio(chunk)));
|
|
1054
851
|
}
|
|
1055
852
|
ngOnDestroy() {
|
|
1056
853
|
this.destroy$.next();
|
|
1057
854
|
this.subscriptions.unsubscribe();
|
|
1058
|
-
this.disconnect();
|
|
855
|
+
void this.disconnect();
|
|
1059
856
|
}
|
|
1060
|
-
/** Reset to idle
|
|
857
|
+
/** Reset to idle (e.g. when modal re-opens so user can click Start Call). */
|
|
1061
858
|
resetToIdle() {
|
|
1062
859
|
if (this.callStateSubject.value === 'idle')
|
|
1063
860
|
return;
|
|
1064
|
-
this.
|
|
1065
|
-
this.stopDurationTimer();
|
|
1066
|
-
this.callStartTime = 0;
|
|
1067
|
-
this.audioAnalyzer.stop();
|
|
1068
|
-
this.stopLocalMic();
|
|
1069
|
-
this.resetRemoteAudioPlayback();
|
|
1070
|
-
this.wsClient.disconnect();
|
|
861
|
+
void this.disconnect();
|
|
1071
862
|
this.callStateSubject.next('idle');
|
|
1072
863
|
this.statusTextSubject.next('');
|
|
1073
864
|
this.durationSubject.next('0:00');
|
|
@@ -1075,7 +866,7 @@ class VoiceAgentService {
|
|
|
1075
866
|
connect(apiUrl, token, botId, conversationId, apiKey, eventToken, eventId, eventUrl, domainAuthority, usersApiUrl) {
|
|
1076
867
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1077
868
|
if (this.callStateSubject.value !== 'idle') {
|
|
1078
|
-
console.warn('Call already in progress');
|
|
869
|
+
console.warn('[HiveGpt Voice] Call already in progress');
|
|
1079
870
|
return;
|
|
1080
871
|
}
|
|
1081
872
|
try {
|
|
@@ -1088,257 +879,204 @@ class VoiceAgentService {
|
|
|
1088
879
|
.ensureValidAccessToken(token, usersApiUrl)
|
|
1089
880
|
.pipe(take(1))
|
|
1090
881
|
.toPromise();
|
|
1091
|
-
if (ensured === null || ensured === void 0 ? void 0 : ensured.accessToken)
|
|
882
|
+
if (ensured === null || ensured === void 0 ? void 0 : ensured.accessToken)
|
|
1092
883
|
accessToken = ensured.accessToken;
|
|
1093
|
-
}
|
|
1094
884
|
}
|
|
1095
885
|
catch (e) {
|
|
1096
|
-
console.warn('[HiveGpt Voice] Token refresh
|
|
886
|
+
console.warn('[HiveGpt Voice] Token refresh failed', e);
|
|
1097
887
|
}
|
|
1098
888
|
}
|
|
1099
889
|
const baseUrl = apiUrl.replace(/\/$/, '');
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
890
|
+
const pcClient = new PipecatClient({
|
|
891
|
+
transport: new WebSocketTransport(),
|
|
892
|
+
enableMic: true,
|
|
893
|
+
enableCam: false,
|
|
894
|
+
callbacks: {
|
|
895
|
+
onConnected: () => this.ngZone.run(() => this.onPipecatConnected()),
|
|
896
|
+
onDisconnected: () => this.ngZone.run(() => this.onPipecatDisconnected()),
|
|
897
|
+
onBotReady: () => this.ngZone.run(() => this.onBotReady()),
|
|
898
|
+
onUserTranscript: (data) => this.ngZone.run(() => this.userTranscriptSubject.next({ text: data.text, final: !!data.final })),
|
|
899
|
+
onBotTranscript: (data) => this.ngZone.run(() => this.botTranscriptSubject.next(data.text)),
|
|
900
|
+
onError: (err) => {
|
|
901
|
+
this.ngZone.run(() => {
|
|
902
|
+
console.error('[HiveGpt Voice] PipecatClient error', err);
|
|
903
|
+
this.callStateSubject.next('ended');
|
|
904
|
+
this.statusTextSubject.next('Connection failed');
|
|
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: {
|
|
1116
949
|
bot_id: botId,
|
|
1117
950
|
conversation_id: conversationId,
|
|
1118
951
|
voice: 'alloy',
|
|
1119
|
-
}
|
|
952
|
+
},
|
|
1120
953
|
});
|
|
1121
|
-
if (!res.ok) {
|
|
1122
|
-
throw new Error(`HTTP ${res.status}`);
|
|
1123
|
-
}
|
|
1124
|
-
const json = yield res.json();
|
|
1125
|
-
const wsUrl = (typeof (json === null || json === void 0 ? void 0 : json.ws_url) === 'string' && json.ws_url) ||
|
|
1126
|
-
(typeof (json === null || json === void 0 ? void 0 : json.rn_ws_url) === 'string' && json.rn_ws_url);
|
|
1127
|
-
if (!wsUrl) {
|
|
1128
|
-
throw new Error('No ws_url in response');
|
|
1129
|
-
}
|
|
1130
|
-
const untilCallEnds$ = merge(this.destroy$, this.endCall$);
|
|
1131
|
-
this.subscriptions.add(this.wsClient.userTranscript$
|
|
1132
|
-
.pipe(takeUntil(untilCallEnds$))
|
|
1133
|
-
.subscribe((t) => this.userTranscriptSubject.next(t)));
|
|
1134
|
-
this.subscriptions.add(this.wsClient.botTranscript$
|
|
1135
|
-
.pipe(takeUntil(untilCallEnds$))
|
|
1136
|
-
.subscribe((t) => this.botTranscriptSubject.next(t)));
|
|
1137
|
-
this.subscriptions.add(this.wsClient.opened$
|
|
1138
|
-
.pipe(takeUntil(untilCallEnds$), take(1))
|
|
1139
|
-
.subscribe(() => void this.onWebsocketOpened()));
|
|
1140
|
-
this.wsClient.connect(wsUrl);
|
|
1141
954
|
}
|
|
1142
955
|
catch (error) {
|
|
1143
|
-
console.error('
|
|
956
|
+
console.error('[HiveGpt Voice] connect failed', error);
|
|
1144
957
|
this.callStateSubject.next('ended');
|
|
1145
|
-
yield this.
|
|
958
|
+
yield this.cleanupPipecatClient();
|
|
1146
959
|
this.statusTextSubject.next('Connection failed');
|
|
1147
960
|
throw error;
|
|
1148
961
|
}
|
|
1149
962
|
});
|
|
1150
963
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
this.callStateSubject.next('connected');
|
|
1160
|
-
this.wireSpeakingState();
|
|
1161
|
-
}
|
|
1162
|
-
catch (err) {
|
|
1163
|
-
console.error('[HiveGpt Voice] Mic or session setup failed', err);
|
|
1164
|
-
this.callStateSubject.next('ended');
|
|
1165
|
-
this.statusTextSubject.next('Microphone unavailable');
|
|
1166
|
-
yield this.disconnect();
|
|
1167
|
-
}
|
|
1168
|
-
});
|
|
964
|
+
onPipecatConnected() {
|
|
965
|
+
// Start the duration timer from the moment the session is live.
|
|
966
|
+
this.callStartTime = Date.now();
|
|
967
|
+
this.startDurationTimer();
|
|
968
|
+
this.callStateSubject.next('connected');
|
|
969
|
+
this.statusTextSubject.next('Connected');
|
|
970
|
+
this.isMicMutedSubject.next(false);
|
|
971
|
+
this.startLocalMicAnalyzer();
|
|
1169
972
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
]).pipe(map(([local, server]) => local || server), distinctUntilChanged(), startWith(false));
|
|
1178
|
-
this.subscriptions.add(combineLatest([assistantTalking$, userTalking$])
|
|
1179
|
-
.pipe(takeUntil(untilCallEnds$))
|
|
1180
|
-
.subscribe(([bot, user]) => {
|
|
1181
|
-
const current = this.callStateSubject.value;
|
|
1182
|
-
if (user) {
|
|
1183
|
-
this.isUserSpeakingSubject.next(true);
|
|
1184
|
-
this.callStateSubject.next('listening');
|
|
1185
|
-
}
|
|
1186
|
-
else {
|
|
1187
|
-
this.isUserSpeakingSubject.next(false);
|
|
1188
|
-
}
|
|
1189
|
-
if (user) {
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
|
-
if (bot) {
|
|
1193
|
-
if (this.callStartTime === 0) {
|
|
1194
|
-
this.callStartTime = Date.now();
|
|
1195
|
-
this.startDurationTimer();
|
|
1196
|
-
}
|
|
1197
|
-
this.callStateSubject.next('talking');
|
|
1198
|
-
}
|
|
1199
|
-
else if (current === 'talking' || current === 'listening') {
|
|
1200
|
-
this.callStateSubject.next('connected');
|
|
1201
|
-
}
|
|
1202
|
-
}));
|
|
1203
|
-
}
|
|
1204
|
-
startLocalMic() {
|
|
1205
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
1206
|
-
this.stopLocalMic();
|
|
1207
|
-
const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
1208
|
-
const track = stream.getAudioTracks()[0];
|
|
1209
|
-
if (!track) {
|
|
1210
|
-
stream.getTracks().forEach((t) => t.stop());
|
|
1211
|
-
throw new Error('No audio track');
|
|
1212
|
-
}
|
|
1213
|
-
this.localMicStream = stream;
|
|
1214
|
-
this.isMicMutedSubject.next(!track.enabled);
|
|
1215
|
-
this.audioAnalyzer.start(stream);
|
|
1216
|
-
});
|
|
1217
|
-
}
|
|
1218
|
-
stopLocalMic() {
|
|
1219
|
-
if (this.localMicStream) {
|
|
1220
|
-
this.localMicStream.getTracks().forEach((t) => t.stop());
|
|
1221
|
-
this.localMicStream = null;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
enqueueRemoteAudio(chunk) {
|
|
1225
|
-
this.pendingRemoteAudio.push(chunk.slice(0));
|
|
1226
|
-
if (!this.remoteAudioPlaying) {
|
|
1227
|
-
void this.playRemoteAudioQueue();
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
playRemoteAudioQueue() {
|
|
1231
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
1232
|
-
this.remoteAudioPlaying = true;
|
|
1233
|
-
const context = this.getOrCreateRemoteAudioContext();
|
|
1234
|
-
while (this.pendingRemoteAudio.length > 0) {
|
|
1235
|
-
const chunk = this.pendingRemoteAudio.shift();
|
|
1236
|
-
if (!chunk)
|
|
1237
|
-
continue;
|
|
1238
|
-
try {
|
|
1239
|
-
const decoded = yield this.decodeAudioChunk(context, chunk);
|
|
1240
|
-
this.assistantAudioStarted();
|
|
1241
|
-
yield this.playDecodedBuffer(context, decoded);
|
|
1242
|
-
}
|
|
1243
|
-
catch (_a) {
|
|
1244
|
-
// Ignore undecodable chunks; server may mix non-audio binary events.
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
this.remoteAudioPlaying = false;
|
|
1248
|
-
this.assistantAudioStopped();
|
|
1249
|
-
});
|
|
973
|
+
onPipecatDisconnected() {
|
|
974
|
+
this.stopDurationTimer();
|
|
975
|
+
this.callStartTime = 0;
|
|
976
|
+
this.audioAnalyzer.stop();
|
|
977
|
+
this.stopBotAudio();
|
|
978
|
+
this.callStateSubject.next('ended');
|
|
979
|
+
this.statusTextSubject.next('Call Ended');
|
|
1250
980
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
981
|
+
onBotReady() {
|
|
982
|
+
var _a, _b, _c;
|
|
983
|
+
// Retry track wiring in case tracks weren't ready at onConnected.
|
|
984
|
+
this.startLocalMicAnalyzer();
|
|
985
|
+
const botTrack = (_c = (_b = (_a = this.pcClient) === null || _a === void 0 ? void 0 : _a.tracks()) === null || _b === void 0 ? void 0 : _b.bot) === null || _c === void 0 ? void 0 : _c.audio;
|
|
986
|
+
if (botTrack)
|
|
987
|
+
this.setupBotAudioTrack(botTrack);
|
|
988
|
+
// Bot is initialised — signal that we're now waiting for user input.
|
|
989
|
+
this.statusTextSubject.next('Listening...');
|
|
990
|
+
}
|
|
991
|
+
startLocalMicAnalyzer() {
|
|
992
|
+
var _a, _b, _c;
|
|
993
|
+
const localTrack = (_c = (_b = (_a = this.pcClient) === null || _a === void 0 ? void 0 : _a.tracks()) === null || _b === void 0 ? void 0 : _b.local) === null || _c === void 0 ? void 0 : _c.audio;
|
|
994
|
+
if (localTrack) {
|
|
995
|
+
this.audioAnalyzer.start(new MediaStream([localTrack]));
|
|
1257
996
|
}
|
|
1258
|
-
return this.remoteAudioContext;
|
|
1259
|
-
}
|
|
1260
|
-
decodeAudioChunk(context, chunk) {
|
|
1261
|
-
return new Promise((resolve, reject) => {
|
|
1262
|
-
context.decodeAudioData(chunk.slice(0), resolve, reject);
|
|
1263
|
-
});
|
|
1264
|
-
}
|
|
1265
|
-
playDecodedBuffer(context, buffer) {
|
|
1266
|
-
return new Promise((resolve) => {
|
|
1267
|
-
const source = context.createBufferSource();
|
|
1268
|
-
source.buffer = buffer;
|
|
1269
|
-
source.connect(context.destination);
|
|
1270
|
-
source.onended = () => resolve();
|
|
1271
|
-
source.start();
|
|
1272
|
-
});
|
|
1273
997
|
}
|
|
1274
|
-
|
|
1275
|
-
if (this.callStartTime === 0) {
|
|
1276
|
-
this.callStartTime = Date.now();
|
|
1277
|
-
this.startDurationTimer();
|
|
1278
|
-
}
|
|
998
|
+
onBotStartedSpeaking() {
|
|
1279
999
|
this.callStateSubject.next('talking');
|
|
1000
|
+
this.statusTextSubject.next('Talking...');
|
|
1001
|
+
// Mark user as no longer speaking when bot takes the turn.
|
|
1002
|
+
this.isUserSpeakingSubject.next(false);
|
|
1280
1003
|
}
|
|
1281
|
-
|
|
1004
|
+
onBotStoppedSpeaking() {
|
|
1282
1005
|
if (this.callStateSubject.value === 'talking') {
|
|
1283
1006
|
this.callStateSubject.next('connected');
|
|
1007
|
+
this.statusTextSubject.next('Listening...');
|
|
1284
1008
|
}
|
|
1285
1009
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
this.
|
|
1289
|
-
|
|
1290
|
-
this.
|
|
1010
|
+
setupBotAudioTrack(track) {
|
|
1011
|
+
var _a;
|
|
1012
|
+
if (!this.botAudioElement) {
|
|
1013
|
+
this.botAudioElement = new Audio();
|
|
1014
|
+
this.botAudioElement.autoplay = true;
|
|
1291
1015
|
}
|
|
1292
|
-
this.
|
|
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));
|
|
1293
1021
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
this.
|
|
1306
|
-
|
|
1307
|
-
});
|
|
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
|
+
}
|
|
1308
1035
|
}
|
|
1309
1036
|
disconnect() {
|
|
1310
1037
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1311
|
-
this.endCall$.next();
|
|
1312
1038
|
this.stopDurationTimer();
|
|
1313
1039
|
this.callStartTime = 0;
|
|
1314
1040
|
this.audioAnalyzer.stop();
|
|
1315
|
-
this.
|
|
1316
|
-
this.
|
|
1317
|
-
this.wsClient.disconnect();
|
|
1041
|
+
this.stopBotAudio();
|
|
1042
|
+
yield this.cleanupPipecatClient();
|
|
1318
1043
|
this.callStateSubject.next('ended');
|
|
1319
1044
|
this.statusTextSubject.next('Call Ended');
|
|
1320
1045
|
});
|
|
1321
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
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1322
1060
|
toggleMic() {
|
|
1323
|
-
|
|
1061
|
+
if (!this.pcClient)
|
|
1062
|
+
return;
|
|
1324
1063
|
const nextMuted = !this.isMicMutedSubject.value;
|
|
1325
|
-
|
|
1326
|
-
if (track) {
|
|
1327
|
-
track.enabled = !nextMuted;
|
|
1328
|
-
}
|
|
1064
|
+
this.pcClient.enableMic(!nextMuted);
|
|
1329
1065
|
this.isMicMutedSubject.next(nextMuted);
|
|
1066
|
+
if (nextMuted)
|
|
1067
|
+
this.isUserSpeakingSubject.next(false);
|
|
1330
1068
|
}
|
|
1331
1069
|
startDurationTimer() {
|
|
1332
|
-
const
|
|
1070
|
+
const tick = () => {
|
|
1333
1071
|
if (this.callStartTime > 0) {
|
|
1334
1072
|
const elapsed = Math.floor((Date.now() - this.callStartTime) / 1000);
|
|
1335
|
-
const
|
|
1336
|
-
const
|
|
1337
|
-
this.durationSubject.next(`${
|
|
1073
|
+
const m = Math.floor(elapsed / 60);
|
|
1074
|
+
const s = elapsed % 60;
|
|
1075
|
+
this.durationSubject.next(`${m}:${String(s).padStart(2, '0')}`);
|
|
1338
1076
|
}
|
|
1339
1077
|
};
|
|
1340
|
-
|
|
1341
|
-
this.durationInterval = setInterval(
|
|
1078
|
+
tick();
|
|
1079
|
+
this.durationInterval = setInterval(tick, 1000);
|
|
1342
1080
|
}
|
|
1343
1081
|
stopDurationTimer() {
|
|
1344
1082
|
if (this.durationInterval) {
|
|
@@ -1347,7 +1085,7 @@ class VoiceAgentService {
|
|
|
1347
1085
|
}
|
|
1348
1086
|
}
|
|
1349
1087
|
}
|
|
1350
|
-
VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService), i0.ɵɵinject(
|
|
1088
|
+
VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService), i0.ɵɵinject(PlatformTokenRefreshService), i0.ɵɵinject(i0.NgZone), i0.ɵɵinject(i0.PLATFORM_ID)); }, token: VoiceAgentService, providedIn: "root" });
|
|
1351
1089
|
VoiceAgentService.decorators = [
|
|
1352
1090
|
{ type: Injectable, args: [{
|
|
1353
1091
|
providedIn: 'root',
|
|
@@ -1355,8 +1093,8 @@ VoiceAgentService.decorators = [
|
|
|
1355
1093
|
];
|
|
1356
1094
|
VoiceAgentService.ctorParameters = () => [
|
|
1357
1095
|
{ type: AudioAnalyzerService },
|
|
1358
|
-
{ type: WebSocketVoiceClientService },
|
|
1359
1096
|
{ type: PlatformTokenRefreshService },
|
|
1097
|
+
{ type: NgZone },
|
|
1360
1098
|
{ type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }
|
|
1361
1099
|
];
|
|
1362
1100
|
|
|
@@ -1387,12 +1125,15 @@ class VoiceAgentModalComponent {
|
|
|
1387
1125
|
this.isMicMuted = false;
|
|
1388
1126
|
this.isUserSpeaking = false;
|
|
1389
1127
|
this.audioLevels = [];
|
|
1390
|
-
this.isSpeaking = false;
|
|
1391
|
-
/** Track whether call has transitioned out of initial connected state. */
|
|
1392
|
-
this.hasLeftConnectedOnce = false;
|
|
1393
1128
|
this.subscriptions = [];
|
|
1394
1129
|
this.isConnecting = false;
|
|
1395
1130
|
}
|
|
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...'; }
|
|
1396
1137
|
ngOnInit() {
|
|
1397
1138
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1398
1139
|
// When opened via Overlay, config is provided by injection
|
|
@@ -1413,16 +1154,8 @@ class VoiceAgentModalComponent {
|
|
|
1413
1154
|
this.agentAvatar = this.injectedConfig.agentAvatar;
|
|
1414
1155
|
this.usersApiUrl = (_h = this.injectedConfig.usersApiUrl) !== null && _h !== void 0 ? _h : this.usersApiUrl;
|
|
1415
1156
|
}
|
|
1416
|
-
// Subscribe to observables
|
|
1417
1157
|
this.subscriptions.push(this.voiceAgentService.callState$.subscribe(state => {
|
|
1418
1158
|
this.callState = state;
|
|
1419
|
-
this.isSpeaking = state === 'talking';
|
|
1420
|
-
if (state === 'listening' || state === 'talking') {
|
|
1421
|
-
this.hasLeftConnectedOnce = true;
|
|
1422
|
-
}
|
|
1423
|
-
if (state === 'idle' || state === 'ended') {
|
|
1424
|
-
this.hasLeftConnectedOnce = false;
|
|
1425
|
-
}
|
|
1426
1159
|
}));
|
|
1427
1160
|
this.subscriptions.push(this.voiceAgentService.statusText$.subscribe(text => {
|
|
1428
1161
|
this.statusText = text;
|
|
@@ -1487,18 +1220,16 @@ class VoiceAgentModalComponent {
|
|
|
1487
1220
|
const maxHeight = 4 + envelope * 38;
|
|
1488
1221
|
return minHeight + (n / 100) * (maxHeight - minHeight);
|
|
1489
1222
|
}
|
|
1490
|
-
/** Status label for active call. */
|
|
1223
|
+
/** Status label for active call — driven by callState + service statusText. */
|
|
1491
1224
|
get statusLabel() {
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
return '
|
|
1496
|
-
|
|
1497
|
-
return '
|
|
1498
|
-
|
|
1499
|
-
return this.hasLeftConnectedOnce ? 'Talking...' : 'Connected';
|
|
1225
|
+
switch (this.callState) {
|
|
1226
|
+
case 'connecting': return 'Connecting...';
|
|
1227
|
+
case 'talking': return 'Talking...';
|
|
1228
|
+
case 'listening': return 'Listening...';
|
|
1229
|
+
// 'connected' covers: initial connect, between turns (Listening / Processing)
|
|
1230
|
+
case 'connected': return this.statusText || 'Connected';
|
|
1231
|
+
default: return this.statusText || '';
|
|
1500
1232
|
}
|
|
1501
|
-
return this.statusText || '';
|
|
1502
1233
|
}
|
|
1503
1234
|
/** Call Again: reset to idle then start a new call. */
|
|
1504
1235
|
callAgain() {
|
|
@@ -1523,8 +1254,8 @@ class VoiceAgentModalComponent {
|
|
|
1523
1254
|
VoiceAgentModalComponent.decorators = [
|
|
1524
1255
|
{ type: Component, args: [{
|
|
1525
1256
|
selector: 'hivegpt-voice-agent-modal',
|
|
1526
|
-
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",
|
|
1527
|
-
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
|
|
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%);transition:opacity .4s ease;width:240px;z-index:-1}.avatar-glow.glow-talking{animation:glowPulse 1.5s ease-in-out infinite;background:radial-gradient(circle,rgba(14,165,164,.35) 0,transparent 65%);height:280px;width:280px}.avatar-glow.glow-listening{background:radial-gradient(circle,rgba(99,102,241,.25) 0,transparent 65%)}@keyframes glowPulse{0%,to{opacity:.7;transform:translate(-50%,-50%) scale(1)}50%{opacity:1;transform:translate(-50%,-50%) scale(1.08)}}.avatar-wrapper.speaking{animation:avatarPulse 1.4s ease-in-out infinite}.avatar-wrapper.listening{animation:avatarListenPulse 1.8s ease-in-out infinite}@keyframes avatarPulse{0%,to{box-shadow:0 0 0 0 rgba(14,165,164,.5)}50%{box-shadow:0 0 0 18px rgba(14,165,164,0)}}@keyframes avatarListenPulse{0%,to{box-shadow:0 0 0 0 rgba(99,102,241,.4)}50%{box-shadow:0 0 0 14px rgba(99,102,241,0)}}.particles-container{height:0;left:50%;pointer-events:none;position:absolute;top:50%;width:0;z-index:2}.particle{animation:particleOrbit 2.4s ease-in-out infinite;animation-delay:var(--delay,0s);background:#0ea5a4;border-radius:50%;height:7px;opacity:0;position:absolute;transform-origin:0 0;width:7px}@keyframes particleOrbit{0%{opacity:0;transform:rotate(calc(var(--i, 0)*22.5deg)) translateY(-108px) scale(.4)}25%{opacity:.9}75%{opacity:.9}to{opacity:0;transform:rotate(calc(var(--i, 0)*22.5deg + 45deg)) translateY(-108px) scale(.4)}}.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{color:#0f172a;font-size:16px;font-weight:400;transition:color .25s ease}.status-text.status-talking{color:#0ea5a4;font-weight:500}.status-text.status-listening{color:#6366f1;font-weight:500}.status-text.status-processing{color:#94a3b8}.status-timer{color:#0f172a;font-size:16px;font-weight:500}.voice-visualizer{align-items:center;display:flex;gap:3px;height:18px}.vbar{animation:vbarBounce 1s ease-in-out infinite;background:#0ea5a4;border-radius:2px;height:6px;width:3px}.vbar:first-child{animation-delay:0s}.vbar:nth-child(2){animation-delay:.15s}.vbar:nth-child(3){animation-delay:.3s}.vbar:nth-child(4){animation-delay:.45s}@keyframes vbarBounce{0%,to{height:4px;opacity:.5}50%{height:16px;opacity:1}}.processing-dots{align-items:center;display:flex;gap:4px}.processing-dots span{animation:dotFade 1.2s ease-in-out infinite;background:#94a3b8;border-radius:50%;display:inline-block;height:5px;width:5px}.processing-dots span:first-child{animation-delay:0s}.processing-dots span:nth-child(2){animation-delay:.2s}.processing-dots span:nth-child(3){animation-delay:.4s}@keyframes dotFade{0%,80%,to{opacity:.4;transform:scale(.8)}40%{opacity:1;transform:scale(1.2)}}.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}"]
|
|
1528
1259
|
},] }
|
|
1529
1260
|
];
|
|
1530
1261
|
VoiceAgentModalComponent.ctorParameters = () => [
|
|
@@ -5395,8 +5126,8 @@ ChatBotComponent.propDecorators = {
|
|
|
5395
5126
|
};
|
|
5396
5127
|
|
|
5397
5128
|
/**
|
|
5398
|
-
* Voice agent module. Uses
|
|
5399
|
-
*
|
|
5129
|
+
* Voice agent module. Uses @pipecat-ai/client-js + @pipecat-ai/websocket-transport
|
|
5130
|
+
* (peer dependencies) for WebSocket transport, RTVI protocol, and audio.
|
|
5400
5131
|
*/
|
|
5401
5132
|
class VoiceAgentModule {
|
|
5402
5133
|
}
|
|
@@ -5411,7 +5142,6 @@ VoiceAgentModule.decorators = [
|
|
|
5411
5142
|
providers: [
|
|
5412
5143
|
VoiceAgentService,
|
|
5413
5144
|
AudioAnalyzerService,
|
|
5414
|
-
WebSocketVoiceClientService
|
|
5415
5145
|
],
|
|
5416
5146
|
exports: [
|
|
5417
5147
|
VoiceAgentModalComponent
|
|
@@ -5682,5 +5412,5 @@ HiveGptModule.decorators = [
|
|
|
5682
5412
|
* Generated bundle index. Do not edit.
|
|
5683
5413
|
*/
|
|
5684
5414
|
|
|
5685
|
-
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,
|
|
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, VideoPlayerComponent as ɵf, SafeHtmlPipe as ɵg, BotHtmlEditorComponent as ɵh };
|
|
5686
5416
|
//# sourceMappingURL=hivegpt-hiveai-angular.js.map
|