@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.
Files changed (23) hide show
  1. package/bundles/hivegpt-hiveai-angular.umd.js +239 -547
  2. package/bundles/hivegpt-hiveai-angular.umd.js.map +1 -1
  3. package/bundles/hivegpt-hiveai-angular.umd.min.js +1 -1
  4. package/bundles/hivegpt-hiveai-angular.umd.min.js.map +1 -1
  5. package/esm2015/hivegpt-hiveai-angular.js +4 -5
  6. package/esm2015/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.js +17 -24
  7. package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +174 -236
  8. package/esm2015/lib/components/voice-agent/voice-agent.module.js +3 -5
  9. package/fesm2015/hivegpt-hiveai-angular.js +190 -460
  10. package/fesm2015/hivegpt-hiveai-angular.js.map +1 -1
  11. package/hivegpt-hiveai-angular.d.ts +3 -4
  12. package/hivegpt-hiveai-angular.d.ts.map +1 -1
  13. package/hivegpt-hiveai-angular.metadata.json +1 -1
  14. package/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts +7 -4
  15. package/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts.map +1 -1
  16. package/lib/components/voice-agent/services/voice-agent.service.d.ts +24 -26
  17. package/lib/components/voice-agent/services/voice-agent.service.d.ts.map +1 -1
  18. package/lib/components/voice-agent/voice-agent.module.d.ts +2 -2
  19. package/lib/components/voice-agent/voice-agent.module.d.ts.map +1 -1
  20. package/package.json +1 -1
  21. package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +0 -206
  22. package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts +0 -59
  23. 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, merge, concat, timer, combineLatest } from 'rxjs';
9
- import { switchMap, catchError, filter, take, map, takeUntil, distinctUntilChanged, startWith, tap } from 'rxjs/operators';
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
- * Native WebSocket client for voice session (signaling, transcripts, speaking hints).
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
- * Connects to `ws_url` from `POST {baseUrl}/ai/ask-voice-socket`.
812
- * Parses JSON messages for transcripts and optional assistant/user speaking flags.
813
- */
814
- class WebSocketVoiceClientService {
815
- constructor(ngZone) {
816
- this.ngZone = ngZone;
817
- this.ws = null;
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, wsClient, platformTokenRefresh,
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.localMicStream = null;
1033
- this.remoteAudioContext = null;
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 state (e.g. when modal opens so user can click Start Call). */
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.endCall$.next();
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 before connect failed', e);
886
+ console.warn('[HiveGpt Voice] Token refresh failed', e);
1097
887
  }
1098
888
  }
1099
889
  const baseUrl = apiUrl.replace(/\/$/, '');
1100
- const postUrl = `${baseUrl}/ai/ask-voice-socket`;
1101
- const headers = {
1102
- 'Content-Type': 'application/json',
1103
- Authorization: `Bearer ${accessToken}`,
1104
- 'x-api-key': apiKey,
1105
- 'hive-bot-id': botId,
1106
- 'domain-authority': domainAuthority,
1107
- eventUrl,
1108
- eventId,
1109
- eventToken,
1110
- 'ngrok-skip-browser-warning': 'true',
1111
- };
1112
- const res = yield fetch(postUrl, {
1113
- method: 'POST',
1114
- headers,
1115
- body: JSON.stringify({
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('Error connecting voice agent:', error);
956
+ console.error('[HiveGpt Voice] connect failed', error);
1144
957
  this.callStateSubject.next('ended');
1145
- yield this.disconnect();
958
+ yield this.cleanupPipecatClient();
1146
959
  this.statusTextSubject.next('Connection failed');
1147
960
  throw error;
1148
961
  }
1149
962
  });
1150
963
  }
1151
- onWebsocketOpened() {
1152
- return __awaiter(this, void 0, void 0, function* () {
1153
- if (this.callStateSubject.value !== 'connecting') {
1154
- return;
1155
- }
1156
- try {
1157
- yield this.startLocalMic();
1158
- this.statusTextSubject.next('Connected');
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
- wireSpeakingState() {
1171
- const untilCallEnds$ = merge(this.destroy$, this.endCall$);
1172
- const transcriptDrivenAssistant$ = this.wsClient.botTranscript$.pipe(switchMap(() => concat(of(true), timer(800).pipe(map(() => false)))), distinctUntilChanged());
1173
- const assistantTalking$ = merge(this.wsClient.assistantSpeaking$, transcriptDrivenAssistant$).pipe(distinctUntilChanged(), startWith(false));
1174
- const userTalking$ = combineLatest([
1175
- this.audioAnalyzer.isUserSpeaking$,
1176
- this.wsClient.serverUserSpeaking$.pipe(startWith(false)),
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
- getOrCreateRemoteAudioContext() {
1252
- if (!this.remoteAudioContext || this.remoteAudioContext.state === 'closed') {
1253
- this.remoteAudioContext = new AudioContext();
1254
- }
1255
- if (this.remoteAudioContext.state === 'suspended') {
1256
- void this.remoteAudioContext.resume();
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
- assistantAudioStarted() {
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
- assistantAudioStopped() {
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
- resetRemoteAudioPlayback() {
1287
- this.pendingRemoteAudio = [];
1288
- this.remoteAudioPlaying = false;
1289
- if (this.remoteAudioContext && this.remoteAudioContext.state !== 'closed') {
1290
- this.remoteAudioContext.close().catch(() => { });
1010
+ setupBotAudioTrack(track) {
1011
+ var _a;
1012
+ if (!this.botAudioElement) {
1013
+ this.botAudioElement = new Audio();
1014
+ this.botAudioElement.autoplay = true;
1291
1015
  }
1292
- this.remoteAudioContext = null;
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
- handleRemoteClose() {
1295
- return __awaiter(this, void 0, void 0, function* () {
1296
- const state = this.callStateSubject.value;
1297
- if (state === 'idle' || state === 'ended')
1298
- return;
1299
- this.endCall$.next();
1300
- this.stopDurationTimer();
1301
- this.callStartTime = 0;
1302
- this.audioAnalyzer.stop();
1303
- this.stopLocalMic();
1304
- this.resetRemoteAudioPlayback();
1305
- this.callStateSubject.next('ended');
1306
- this.statusTextSubject.next('Connection lost');
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.stopLocalMic();
1316
- this.resetRemoteAudioPlayback();
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
- var _a;
1061
+ if (!this.pcClient)
1062
+ return;
1324
1063
  const nextMuted = !this.isMicMutedSubject.value;
1325
- const track = (_a = this.localMicStream) === null || _a === void 0 ? void 0 : _a.getAudioTracks()[0];
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 updateDuration = () => {
1070
+ const tick = () => {
1333
1071
  if (this.callStartTime > 0) {
1334
1072
  const elapsed = Math.floor((Date.now() - this.callStartTime) / 1000);
1335
- const minutes = Math.floor(elapsed / 60);
1336
- const seconds = elapsed % 60;
1337
- this.durationSubject.next(`${minutes}:${String(seconds).padStart(2, '0')}`);
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
- updateDuration();
1341
- this.durationInterval = setInterval(updateDuration, 1000);
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(WebSocketVoiceClientService), i0.ɵɵinject(PlatformTokenRefreshService), i0.ɵɵinject(i0.PLATFORM_ID)); }, token: VoiceAgentService, providedIn: "root" });
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
- if (this.callState === 'connecting')
1493
- return this.statusText || 'Connecting...';
1494
- if (this.callState === 'talking')
1495
- return 'Talking...';
1496
- if (this.callState === 'listening')
1497
- return 'Listening';
1498
- if (this.callState === 'connected') {
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 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}"]
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 native WebSocket for the voice session.
5399
- * Does NOT use Socket.IO or ngx-socket-io.
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, WebSocketVoiceClientService as ɵf, VideoPlayerComponent as ɵg, SafeHtmlPipe as ɵh, BotHtmlEditorComponent as ɵi };
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