@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
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/cdk/overlay'), require('@angular/cdk/portal'), require('@angular/common/http'), require('@angular/core'), require('@angular/platform-browser'), require('rxjs'), require('rxjs/operators'), require('@angular/common'), require('ngx-socket-io'), require('@angular/forms'), require('microsoft-cognitiveservices-speech-sdk'), require('marked'), require('@angular/material/icon'), require('@angular/material/sidenav'), require('ngx-quill')) :
3
- typeof define === 'function' && define.amd ? define('@hivegpt/hiveai-angular', ['exports', '@angular/cdk/overlay', '@angular/cdk/portal', '@angular/common/http', '@angular/core', '@angular/platform-browser', 'rxjs', 'rxjs/operators', '@angular/common', 'ngx-socket-io', '@angular/forms', 'microsoft-cognitiveservices-speech-sdk', 'marked', '@angular/material/icon', '@angular/material/sidenav', 'ngx-quill'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.hivegpt = global.hivegpt || {}, global.hivegpt["hiveai-angular"] = {}), global.ng.cdk.overlay, global.ng.cdk.portal, global.ng.common.http, global.ng.core, global.ng.platformBrowser, global.rxjs, global.rxjs.operators, global.ng.common, global.ngxSocketIo, global.ng.forms, global.SpeechSDK, global.marked, global.ng.material.icon, global.ng.material.sidenav, global.ngxQuill));
5
- })(this, (function (exports, overlay, portal, i1, i0, platformBrowser, rxjs, operators, common, ngxSocketIo, forms, SpeechSDK, marked, icon, sidenav, ngxQuill) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/cdk/overlay'), require('@angular/cdk/portal'), require('@angular/common/http'), require('@angular/core'), require('@angular/platform-browser'), require('rxjs'), require('rxjs/operators'), require('@angular/common'), require('ngx-socket-io'), require('@angular/forms'), require('microsoft-cognitiveservices-speech-sdk'), require('marked'), require('@pipecat-ai/client-js'), require('@pipecat-ai/websocket-transport'), require('@angular/material/icon'), require('@angular/material/sidenav'), require('ngx-quill')) :
3
+ typeof define === 'function' && define.amd ? define('@hivegpt/hiveai-angular', ['exports', '@angular/cdk/overlay', '@angular/cdk/portal', '@angular/common/http', '@angular/core', '@angular/platform-browser', 'rxjs', 'rxjs/operators', '@angular/common', 'ngx-socket-io', '@angular/forms', 'microsoft-cognitiveservices-speech-sdk', 'marked', '@pipecat-ai/client-js', '@pipecat-ai/websocket-transport', '@angular/material/icon', '@angular/material/sidenav', 'ngx-quill'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.hivegpt = global.hivegpt || {}, global.hivegpt["hiveai-angular"] = {}), global.ng.cdk.overlay, global.ng.cdk.portal, global.ng.common.http, global.ng.core, global.ng.platformBrowser, global.rxjs, global.rxjs.operators, global.ng.common, global.ngxSocketIo, global.ng.forms, global.SpeechSDK, global.marked, global.clientJs, global.websocketTransport, global.ng.material.icon, global.ng.material.sidenav, global.ngxQuill));
5
+ })(this, (function (exports, overlay, portal, i1, i0, platformBrowser, rxjs, operators, common, ngxSocketIo, forms, SpeechSDK, marked, clientJs, websocketTransport, icon, sidenav, ngxQuill) { 'use strict';
6
6
 
7
7
  function _interopNamespace(e) {
8
8
  if (e && e.__esModule) return e;
@@ -1339,237 +1339,24 @@
1339
1339
  ];
1340
1340
 
1341
1341
  /**
1342
- * Native WebSocket client for voice session (signaling, transcripts, speaking hints).
1343
- * CRITICAL: Uses native WebSocket only. NO Socket.IO, NO ngx-socket-io.
1342
+ * Voice agent orchestrator using the official PipecatClient SDK.
1344
1343
  *
1345
- * Connects to `ws_url` from `POST {baseUrl}/ai/ask-voice-socket`.
1346
- * Parses JSON messages for transcripts and optional assistant/user speaking flags.
1347
- */
1348
- var WebSocketVoiceClientService = /** @class */ (function () {
1349
- function WebSocketVoiceClientService(ngZone) {
1350
- this.ngZone = ngZone;
1351
- this.ws = null;
1352
- /** True when {@link disconnect} initiated the close (not counted as remote close). */
1353
- this.closeInitiatedByClient = false;
1354
- this.openedSubject = new rxjs.Subject();
1355
- this.remoteCloseSubject = new rxjs.Subject();
1356
- this.userTranscriptSubject = new rxjs.Subject();
1357
- this.botTranscriptSubject = new rxjs.Subject();
1358
- this.assistantSpeakingSubject = new rxjs.Subject();
1359
- this.serverUserSpeakingSubject = new rxjs.Subject();
1360
- this.audioChunkSubject = new rxjs.Subject();
1361
- /** Fires once each time the WebSocket reaches OPEN. */
1362
- this.opened$ = this.openedSubject.asObservable();
1363
- /** Fires when the socket closes without a client-initiated {@link disconnect}. */
1364
- this.remoteClose$ = this.remoteCloseSubject.asObservable();
1365
- this.userTranscript$ = this.userTranscriptSubject.asObservable();
1366
- this.botTranscript$ = this.botTranscriptSubject.asObservable();
1367
- /** Assistant/bot speaking, when the server sends explicit events (see {@link handleJsonMessage}). */
1368
- this.assistantSpeaking$ = this.assistantSpeakingSubject.asObservable();
1369
- /** User speaking from server-side VAD, if provided. */
1370
- this.serverUserSpeaking$ = this.serverUserSpeakingSubject.asObservable();
1371
- /** Binary audio frames from server (when backend streams bot audio over WS). */
1372
- this.audioChunk$ = this.audioChunkSubject.asObservable();
1373
- }
1374
- WebSocketVoiceClientService.prototype.connect = function (wsUrl) {
1375
- var _this = this;
1376
- var _a;
1377
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
1378
- return;
1379
- }
1380
- if (this.ws) {
1381
- this.closeInitiatedByClient = true;
1382
- this.ws.close();
1383
- }
1384
- try {
1385
- var socket_1 = new WebSocket(wsUrl);
1386
- this.ws = socket_1;
1387
- socket_1.onopen = function () {
1388
- if (_this.ws !== socket_1)
1389
- return;
1390
- _this.ngZone.run(function () { return _this.openedSubject.next(); });
1391
- };
1392
- socket_1.onmessage = function (event) {
1393
- if (_this.ws !== socket_1)
1394
- return;
1395
- void _this.handleIncomingMessage(event.data);
1396
- };
1397
- socket_1.onerror = function () {
1398
- _this.ngZone.run(function () {
1399
- if (_this.ws === socket_1 && socket_1.readyState !== WebSocket.CLOSED) {
1400
- socket_1.close();
1401
- }
1402
- });
1403
- };
1404
- socket_1.onclose = function () {
1405
- if (_this.ws === socket_1) {
1406
- _this.ws = null;
1407
- }
1408
- var client = _this.closeInitiatedByClient;
1409
- _this.closeInitiatedByClient = false;
1410
- if (!client) {
1411
- _this.ngZone.run(function () { return _this.remoteCloseSubject.next(); });
1412
- }
1413
- };
1414
- }
1415
- catch (err) {
1416
- console.error('WebSocketVoiceClient: connect failed', err);
1417
- this.ws = null;
1418
- throw err;
1419
- }
1420
- };
1421
- WebSocketVoiceClientService.prototype.handleIncomingMessage = function (payload) {
1422
- return __awaiter(this, void 0, void 0, function () {
1423
- var ab;
1424
- return __generator(this, function (_b) {
1425
- switch (_b.label) {
1426
- case 0:
1427
- if (typeof payload === 'string') {
1428
- this.handleJsonString(payload);
1429
- return [2 /*return*/];
1430
- }
1431
- if (payload instanceof ArrayBuffer) {
1432
- this.handleBinaryMessage(payload);
1433
- return [2 /*return*/];
1434
- }
1435
- if (!(payload instanceof Blob)) return [3 /*break*/, 2];
1436
- return [4 /*yield*/, payload.arrayBuffer()];
1437
- case 1:
1438
- ab = _b.sent();
1439
- this.handleBinaryMessage(ab);
1440
- _b.label = 2;
1441
- case 2: return [2 /*return*/];
1442
- }
1443
- });
1444
- });
1445
- };
1446
- WebSocketVoiceClientService.prototype.handleJsonString = function (jsonText) {
1447
- var _this = this;
1448
- try {
1449
- var msg_1 = JSON.parse(jsonText);
1450
- this.ngZone.run(function () { return _this.handleJsonMessage(msg_1); });
1451
- }
1452
- catch (_a) {
1453
- // Ignore non-JSON
1454
- }
1455
- };
1456
- WebSocketVoiceClientService.prototype.handleBinaryMessage = function (buffer) {
1457
- var _this = this;
1458
- // Some backends wrap JSON events inside binary WS frames.
1459
- var maybeText = this.tryDecodeUtf8(buffer);
1460
- if (maybeText !== null) {
1461
- this.handleJsonString(maybeText);
1462
- return;
1463
- }
1464
- // Otherwise treat binary as streamed assistant audio.
1465
- this.ngZone.run(function () { return _this.audioChunkSubject.next(buffer); });
1466
- };
1467
- WebSocketVoiceClientService.prototype.tryDecodeUtf8 = function (buffer) {
1468
- try {
1469
- var text = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
1470
- var trimmed = text.trim();
1471
- if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) {
1472
- return null;
1473
- }
1474
- return trimmed;
1475
- }
1476
- catch (_a) {
1477
- return null;
1478
- }
1479
- };
1480
- WebSocketVoiceClientService.prototype.handleJsonMessage = function (msg) {
1481
- var type = msg.type;
1482
- var typeStr = typeof type === 'string' ? type : '';
1483
- if (typeStr === 'session_ready' || typeStr === 'connected' || typeStr === 'voice_session_started') {
1484
- return;
1485
- }
1486
- if (typeStr === 'assistant_speaking' ||
1487
- typeStr === 'bot_speaking') {
1488
- if (msg.active === true || msg.speaking === true) {
1489
- this.assistantSpeakingSubject.next(true);
1490
- }
1491
- else if (msg.active === false || msg.speaking === false) {
1492
- this.assistantSpeakingSubject.next(false);
1493
- }
1494
- return;
1495
- }
1496
- if (typeStr === 'user_speaking') {
1497
- if (msg.active === true || msg.speaking === true) {
1498
- this.serverUserSpeakingSubject.next(true);
1499
- }
1500
- else if (msg.active === false || msg.speaking === false) {
1501
- this.serverUserSpeakingSubject.next(false);
1502
- }
1503
- return;
1504
- }
1505
- if (typeStr === 'input_audio_buffer.speech_started') {
1506
- this.serverUserSpeakingSubject.next(true);
1507
- return;
1508
- }
1509
- if (typeStr === 'input_audio_buffer.speech_stopped') {
1510
- this.serverUserSpeakingSubject.next(false);
1511
- return;
1512
- }
1513
- if (typeStr === 'response.audio.delta') {
1514
- this.assistantSpeakingSubject.next(true);
1515
- return;
1516
- }
1517
- if (typeStr === 'response.audio.done' ||
1518
- typeStr === 'response.output_audio.done') {
1519
- this.assistantSpeakingSubject.next(false);
1520
- return;
1521
- }
1522
- if (typeStr === 'user_transcript' && typeof msg.text === 'string') {
1523
- this.userTranscriptSubject.next({
1524
- text: msg.text,
1525
- final: msg.final === true,
1526
- });
1527
- return;
1528
- }
1529
- if (typeStr === 'bot_transcript' && typeof msg.text === 'string') {
1530
- this.botTranscriptSubject.next(msg.text);
1531
- }
1532
- };
1533
- WebSocketVoiceClientService.prototype.disconnect = function () {
1534
- if (!this.ws) {
1535
- return;
1536
- }
1537
- this.closeInitiatedByClient = true;
1538
- this.ws.close();
1539
- };
1540
- Object.defineProperty(WebSocketVoiceClientService.prototype, "isConnected", {
1541
- get: function () {
1542
- var _a;
1543
- return ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN;
1544
- },
1545
- enumerable: false,
1546
- configurable: true
1547
- });
1548
- return WebSocketVoiceClientService;
1549
- }());
1550
- WebSocketVoiceClientService.ɵprov = i0__namespace.ɵɵdefineInjectable({ factory: function WebSocketVoiceClientService_Factory() { return new WebSocketVoiceClientService(i0__namespace.ɵɵinject(i0__namespace.NgZone)); }, token: WebSocketVoiceClientService, providedIn: "root" });
1551
- WebSocketVoiceClientService.decorators = [
1552
- { type: i0.Injectable, args: [{
1553
- providedIn: 'root',
1554
- },] }
1555
- ];
1556
- WebSocketVoiceClientService.ctorParameters = function () { return [
1557
- { type: i0.NgZone }
1558
- ]; };
1559
-
1560
- /**
1561
- * Voice agent orchestrator: single WebSocket (`ws_url` from POST /ai/ask-voice-socket)
1562
- * for session events, transcripts, and optional speaking hints; local mic for capture
1563
- * and waveform only (no Daily/WebRTC room).
1344
+ * Audio flow (mirrors the React reference implementation):
1345
+ * - Local mic: acquired by PipecatClient.initDevices(); local track fed to
1346
+ * AudioAnalyzerService for waveform visualisation.
1347
+ * - Bot audio: received as a MediaStreamTrack via RTVIEvent.TrackStarted,
1348
+ * played through a hidden <audio> element.
1349
+ * - All binary protobuf framing / RTVI protocol handled by
1350
+ * @pipecat-ai/client-js + @pipecat-ai/websocket-transport.
1564
1351
  */
1565
1352
  var VoiceAgentService = /** @class */ (function () {
1566
- function VoiceAgentService(audioAnalyzer, wsClient, platformTokenRefresh,
1353
+ function VoiceAgentService(audioAnalyzer, platformTokenRefresh, ngZone,
1567
1354
  /** `Object` not `object` — ngc metadata collection rejects the `object` type in DI params. */
1568
1355
  platformId) {
1569
1356
  var _this = this;
1570
1357
  this.audioAnalyzer = audioAnalyzer;
1571
- this.wsClient = wsClient;
1572
1358
  this.platformTokenRefresh = platformTokenRefresh;
1359
+ this.ngZone = ngZone;
1573
1360
  this.platformId = platformId;
1574
1361
  this.callStateSubject = new rxjs.BehaviorSubject('idle');
1575
1362
  this.statusTextSubject = new rxjs.BehaviorSubject('');
@@ -1581,11 +1368,8 @@
1581
1368
  this.botTranscriptSubject = new rxjs.Subject();
1582
1369
  this.callStartTime = 0;
1583
1370
  this.durationInterval = null;
1584
- this.localMicStream = null;
1585
- this.remoteAudioContext = null;
1586
- this.pendingRemoteAudio = [];
1587
- this.remoteAudioPlaying = false;
1588
- this.endCall$ = new rxjs.Subject();
1371
+ this.pcClient = null;
1372
+ this.botAudioElement = null;
1589
1373
  this.subscriptions = new rxjs.Subscription();
1590
1374
  this.destroy$ = new rxjs.Subject();
1591
1375
  this.callState$ = this.callStateSubject.asObservable();
@@ -1597,123 +1381,134 @@
1597
1381
  this.userTranscript$ = this.userTranscriptSubject.asObservable();
1598
1382
  this.botTranscript$ = this.botTranscriptSubject.asObservable();
1599
1383
  this.subscriptions.add(this.audioAnalyzer.audioLevels$.subscribe(function (levels) { return _this.audioLevelsSubject.next(levels); }));
1600
- this.subscriptions.add(this.wsClient.remoteClose$
1601
- .pipe(operators.takeUntil(this.destroy$))
1602
- .subscribe(function () { return void _this.handleRemoteClose(); }));
1603
- this.subscriptions.add(this.wsClient.audioChunk$
1604
- .pipe(operators.takeUntil(this.destroy$))
1605
- .subscribe(function (chunk) { return _this.enqueueRemoteAudio(chunk); }));
1606
1384
  }
1607
1385
  VoiceAgentService.prototype.ngOnDestroy = function () {
1608
1386
  this.destroy$.next();
1609
1387
  this.subscriptions.unsubscribe();
1610
- this.disconnect();
1388
+ void this.disconnect();
1611
1389
  };
1612
- /** Reset to idle state (e.g. when modal opens so user can click Start Call). */
1390
+ /** Reset to idle (e.g. when modal re-opens so user can click Start Call). */
1613
1391
  VoiceAgentService.prototype.resetToIdle = function () {
1614
1392
  if (this.callStateSubject.value === 'idle')
1615
1393
  return;
1616
- this.endCall$.next();
1617
- this.stopDurationTimer();
1618
- this.callStartTime = 0;
1619
- this.audioAnalyzer.stop();
1620
- this.stopLocalMic();
1621
- this.resetRemoteAudioPlayback();
1622
- this.wsClient.disconnect();
1394
+ void this.disconnect();
1623
1395
  this.callStateSubject.next('idle');
1624
1396
  this.statusTextSubject.next('');
1625
1397
  this.durationSubject.next('0:00');
1626
1398
  };
1627
1399
  VoiceAgentService.prototype.connect = function (apiUrl, token, botId, conversationId, apiKey, eventToken, eventId, eventUrl, domainAuthority, usersApiUrl) {
1628
1400
  return __awaiter(this, void 0, void 0, function () {
1629
- var accessToken, ensured, e_1, baseUrl, postUrl, headers, res, json, wsUrl, untilCallEnds$, error_1;
1401
+ var accessToken, ensured, e_1, baseUrl, pcClient, requestHeaders, error_1;
1630
1402
  var _this = this;
1631
- return __generator(this, function (_b) {
1632
- switch (_b.label) {
1403
+ return __generator(this, function (_d) {
1404
+ switch (_d.label) {
1633
1405
  case 0:
1634
1406
  if (this.callStateSubject.value !== 'idle') {
1635
- console.warn('Call already in progress');
1407
+ console.warn('[HiveGpt Voice] Call already in progress');
1636
1408
  return [2 /*return*/];
1637
1409
  }
1638
- _b.label = 1;
1410
+ _d.label = 1;
1639
1411
  case 1:
1640
- _b.trys.push([1, 8, , 10]);
1412
+ _d.trys.push([1, 8, , 10]);
1641
1413
  this.callStateSubject.next('connecting');
1642
1414
  this.statusTextSubject.next('Connecting...');
1643
1415
  accessToken = token;
1644
1416
  if (!(usersApiUrl && common.isPlatformBrowser(this.platformId))) return [3 /*break*/, 5];
1645
- _b.label = 2;
1417
+ _d.label = 2;
1646
1418
  case 2:
1647
- _b.trys.push([2, 4, , 5]);
1419
+ _d.trys.push([2, 4, , 5]);
1648
1420
  return [4 /*yield*/, this.platformTokenRefresh
1649
1421
  .ensureValidAccessToken(token, usersApiUrl)
1650
1422
  .pipe(operators.take(1))
1651
1423
  .toPromise()];
1652
1424
  case 3:
1653
- ensured = _b.sent();
1654
- if (ensured === null || ensured === void 0 ? void 0 : ensured.accessToken) {
1425
+ ensured = _d.sent();
1426
+ if (ensured === null || ensured === void 0 ? void 0 : ensured.accessToken)
1655
1427
  accessToken = ensured.accessToken;
1656
- }
1657
1428
  return [3 /*break*/, 5];
1658
1429
  case 4:
1659
- e_1 = _b.sent();
1660
- console.warn('[HiveGpt Voice] Token refresh before connect failed', e_1);
1430
+ e_1 = _d.sent();
1431
+ console.warn('[HiveGpt Voice] Token refresh failed', e_1);
1661
1432
  return [3 /*break*/, 5];
1662
1433
  case 5:
1663
1434
  baseUrl = apiUrl.replace(/\/$/, '');
1664
- postUrl = baseUrl + "/ai/ask-voice-socket";
1665
- headers = {
1666
- 'Content-Type': 'application/json',
1667
- Authorization: "Bearer " + accessToken,
1668
- 'x-api-key': apiKey,
1669
- 'hive-bot-id': botId,
1670
- 'domain-authority': domainAuthority,
1671
- eventUrl: eventUrl,
1672
- eventId: eventId,
1673
- eventToken: eventToken,
1674
- 'ngrok-skip-browser-warning': 'true',
1675
- };
1676
- return [4 /*yield*/, fetch(postUrl, {
1677
- method: 'POST',
1678
- headers: headers,
1679
- body: JSON.stringify({
1435
+ pcClient = new clientJs.PipecatClient({
1436
+ transport: new websocketTransport.WebSocketTransport(),
1437
+ enableMic: true,
1438
+ enableCam: false,
1439
+ callbacks: {
1440
+ onConnected: function () { return _this.ngZone.run(function () { return _this.onPipecatConnected(); }); },
1441
+ onDisconnected: function () { return _this.ngZone.run(function () { return _this.onPipecatDisconnected(); }); },
1442
+ onBotReady: function () { return _this.ngZone.run(function () { return _this.onBotReady(); }); },
1443
+ onUserTranscript: function (data) { return _this.ngZone.run(function () { return _this.userTranscriptSubject.next({ text: data.text, final: !!data.final }); }); },
1444
+ onBotTranscript: function (data) { return _this.ngZone.run(function () { return _this.botTranscriptSubject.next(data.text); }); },
1445
+ onError: function (err) {
1446
+ _this.ngZone.run(function () {
1447
+ console.error('[HiveGpt Voice] PipecatClient error', err);
1448
+ _this.callStateSubject.next('ended');
1449
+ _this.statusTextSubject.next('Connection failed');
1450
+ });
1451
+ },
1452
+ },
1453
+ });
1454
+ this.pcClient = pcClient;
1455
+ // Bot audio arrives as a MediaStreamTrack — wire to a hidden <audio> element
1456
+ pcClient.on(clientJs.RTVIEvent.TrackStarted, function (track, participant) {
1457
+ if (!(participant === null || participant === void 0 ? void 0 : participant.local) && track.kind === 'audio') {
1458
+ _this.ngZone.run(function () { return _this.setupBotAudioTrack(track); });
1459
+ }
1460
+ });
1461
+ // Speaking state comes straight from RTVI events
1462
+ pcClient.on(clientJs.RTVIEvent.BotStartedSpeaking, function () { return _this.ngZone.run(function () { return _this.onBotStartedSpeaking(); }); });
1463
+ pcClient.on(clientJs.RTVIEvent.BotStoppedSpeaking, function () { return _this.ngZone.run(function () { return _this.onBotStoppedSpeaking(); }); });
1464
+ pcClient.on(clientJs.RTVIEvent.UserStartedSpeaking, function () { return _this.ngZone.run(function () {
1465
+ _this.isUserSpeakingSubject.next(true);
1466
+ _this.callStateSubject.next('listening');
1467
+ _this.statusTextSubject.next('Listening...');
1468
+ }); });
1469
+ pcClient.on(clientJs.RTVIEvent.UserStoppedSpeaking, function () { return _this.ngZone.run(function () {
1470
+ _this.isUserSpeakingSubject.next(false);
1471
+ if (_this.callStateSubject.value === 'listening') {
1472
+ // Brief 'Processing...' while we wait for the bot to respond.
1473
+ _this.callStateSubject.next('connected');
1474
+ _this.statusTextSubject.next('Processing...');
1475
+ }
1476
+ }); });
1477
+ // Acquire mic (triggers browser permission prompt)
1478
+ return [4 /*yield*/, pcClient.initDevices()];
1479
+ case 6:
1480
+ // Acquire mic (triggers browser permission prompt)
1481
+ _d.sent();
1482
+ requestHeaders = new Headers();
1483
+ requestHeaders.append('Authorization', "Bearer " + accessToken);
1484
+ requestHeaders.append('x-api-key', apiKey);
1485
+ requestHeaders.append('hive-bot-id', botId);
1486
+ requestHeaders.append('domain-authority', domainAuthority);
1487
+ requestHeaders.append('eventUrl', eventUrl);
1488
+ requestHeaders.append('eventId', eventId);
1489
+ requestHeaders.append('eventToken', eventToken);
1490
+ requestHeaders.append('ngrok-skip-browser-warning', 'true');
1491
+ // POST to /ai/ask-voice-socket → receives { ws_url } → WebSocketTransport connects
1492
+ return [4 /*yield*/, pcClient.startBotAndConnect({
1493
+ endpoint: baseUrl + "/ai/ask-voice-socket",
1494
+ headers: requestHeaders,
1495
+ requestData: {
1680
1496
  bot_id: botId,
1681
1497
  conversation_id: conversationId,
1682
1498
  voice: 'alloy',
1683
- }),
1499
+ },
1684
1500
  })];
1685
- case 6:
1686
- res = _b.sent();
1687
- if (!res.ok) {
1688
- throw new Error("HTTP " + res.status);
1689
- }
1690
- return [4 /*yield*/, res.json()];
1691
1501
  case 7:
1692
- json = _b.sent();
1693
- wsUrl = (typeof (json === null || json === void 0 ? void 0 : json.ws_url) === 'string' && json.ws_url) ||
1694
- (typeof (json === null || json === void 0 ? void 0 : json.rn_ws_url) === 'string' && json.rn_ws_url);
1695
- if (!wsUrl) {
1696
- throw new Error('No ws_url in response');
1697
- }
1698
- untilCallEnds$ = rxjs.merge(this.destroy$, this.endCall$);
1699
- this.subscriptions.add(this.wsClient.userTranscript$
1700
- .pipe(operators.takeUntil(untilCallEnds$))
1701
- .subscribe(function (t) { return _this.userTranscriptSubject.next(t); }));
1702
- this.subscriptions.add(this.wsClient.botTranscript$
1703
- .pipe(operators.takeUntil(untilCallEnds$))
1704
- .subscribe(function (t) { return _this.botTranscriptSubject.next(t); }));
1705
- this.subscriptions.add(this.wsClient.opened$
1706
- .pipe(operators.takeUntil(untilCallEnds$), operators.take(1))
1707
- .subscribe(function () { return void _this.onWebsocketOpened(); }));
1708
- this.wsClient.connect(wsUrl);
1502
+ // POST to /ai/ask-voice-socket → receives { ws_url } → WebSocketTransport connects
1503
+ _d.sent();
1709
1504
  return [3 /*break*/, 10];
1710
1505
  case 8:
1711
- error_1 = _b.sent();
1712
- console.error('Error connecting voice agent:', error_1);
1506
+ error_1 = _d.sent();
1507
+ console.error('[HiveGpt Voice] connect failed', error_1);
1713
1508
  this.callStateSubject.next('ended');
1714
- return [4 /*yield*/, this.disconnect()];
1509
+ return [4 /*yield*/, this.cleanupPipecatClient()];
1715
1510
  case 9:
1716
- _b.sent();
1511
+ _d.sent();
1717
1512
  this.statusTextSubject.next('Connection failed');
1718
1513
  throw error_1;
1719
1514
  case 10: return [2 /*return*/];
@@ -1721,249 +1516,143 @@
1721
1516
  });
1722
1517
  });
1723
1518
  };
1724
- VoiceAgentService.prototype.onWebsocketOpened = function () {
1725
- return __awaiter(this, void 0, void 0, function () {
1726
- var err_1;
1727
- return __generator(this, function (_b) {
1728
- switch (_b.label) {
1729
- case 0:
1730
- if (this.callStateSubject.value !== 'connecting') {
1731
- return [2 /*return*/];
1732
- }
1733
- _b.label = 1;
1734
- case 1:
1735
- _b.trys.push([1, 3, , 5]);
1736
- return [4 /*yield*/, this.startLocalMic()];
1737
- case 2:
1738
- _b.sent();
1739
- this.statusTextSubject.next('Connected');
1740
- this.callStateSubject.next('connected');
1741
- this.wireSpeakingState();
1742
- return [3 /*break*/, 5];
1743
- case 3:
1744
- err_1 = _b.sent();
1745
- console.error('[HiveGpt Voice] Mic or session setup failed', err_1);
1746
- this.callStateSubject.next('ended');
1747
- this.statusTextSubject.next('Microphone unavailable');
1748
- return [4 /*yield*/, this.disconnect()];
1749
- case 4:
1750
- _b.sent();
1751
- return [3 /*break*/, 5];
1752
- case 5: return [2 /*return*/];
1753
- }
1754
- });
1755
- });
1519
+ VoiceAgentService.prototype.onPipecatConnected = function () {
1520
+ // Start the duration timer from the moment the session is live.
1521
+ this.callStartTime = Date.now();
1522
+ this.startDurationTimer();
1523
+ this.callStateSubject.next('connected');
1524
+ this.statusTextSubject.next('Connected');
1525
+ this.isMicMutedSubject.next(false);
1526
+ this.startLocalMicAnalyzer();
1756
1527
  };
1757
- VoiceAgentService.prototype.wireSpeakingState = function () {
1758
- var _this = this;
1759
- var untilCallEnds$ = rxjs.merge(this.destroy$, this.endCall$);
1760
- var transcriptDrivenAssistant$ = this.wsClient.botTranscript$.pipe(operators.switchMap(function () { return rxjs.concat(rxjs.of(true), rxjs.timer(800).pipe(operators.map(function () { return false; }))); }), operators.distinctUntilChanged());
1761
- var assistantTalking$ = rxjs.merge(this.wsClient.assistantSpeaking$, transcriptDrivenAssistant$).pipe(operators.distinctUntilChanged(), operators.startWith(false));
1762
- var userTalking$ = rxjs.combineLatest([
1763
- this.audioAnalyzer.isUserSpeaking$,
1764
- this.wsClient.serverUserSpeaking$.pipe(operators.startWith(false)),
1765
- ]).pipe(operators.map(function (_b) {
1766
- var _c = __read(_b, 2), local = _c[0], server = _c[1];
1767
- return local || server;
1768
- }), operators.distinctUntilChanged(), operators.startWith(false));
1769
- this.subscriptions.add(rxjs.combineLatest([assistantTalking$, userTalking$])
1770
- .pipe(operators.takeUntil(untilCallEnds$))
1771
- .subscribe(function (_b) {
1772
- var _c = __read(_b, 2), bot = _c[0], user = _c[1];
1773
- var current = _this.callStateSubject.value;
1774
- if (user) {
1775
- _this.isUserSpeakingSubject.next(true);
1776
- _this.callStateSubject.next('listening');
1777
- }
1778
- else {
1779
- _this.isUserSpeakingSubject.next(false);
1780
- }
1781
- if (user) {
1782
- return;
1783
- }
1784
- if (bot) {
1785
- if (_this.callStartTime === 0) {
1786
- _this.callStartTime = Date.now();
1787
- _this.startDurationTimer();
1788
- }
1789
- _this.callStateSubject.next('talking');
1528
+ VoiceAgentService.prototype.onPipecatDisconnected = function () {
1529
+ this.stopDurationTimer();
1530
+ this.callStartTime = 0;
1531
+ this.audioAnalyzer.stop();
1532
+ this.stopBotAudio();
1533
+ this.callStateSubject.next('ended');
1534
+ this.statusTextSubject.next('Call Ended');
1535
+ };
1536
+ VoiceAgentService.prototype.onBotReady = function () {
1537
+ var _a, _b, _c;
1538
+ // Retry track wiring in case tracks weren't ready at onConnected.
1539
+ this.startLocalMicAnalyzer();
1540
+ var 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;
1541
+ if (botTrack)
1542
+ this.setupBotAudioTrack(botTrack);
1543
+ // Bot is initialised signal that we're now waiting for user input.
1544
+ this.statusTextSubject.next('Listening...');
1545
+ };
1546
+ VoiceAgentService.prototype.startLocalMicAnalyzer = function () {
1547
+ var _a, _b, _c;
1548
+ var 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;
1549
+ if (localTrack) {
1550
+ this.audioAnalyzer.start(new MediaStream([localTrack]));
1551
+ }
1552
+ };
1553
+ VoiceAgentService.prototype.onBotStartedSpeaking = function () {
1554
+ this.callStateSubject.next('talking');
1555
+ this.statusTextSubject.next('Talking...');
1556
+ // Mark user as no longer speaking when bot takes the turn.
1557
+ this.isUserSpeakingSubject.next(false);
1558
+ };
1559
+ VoiceAgentService.prototype.onBotStoppedSpeaking = function () {
1560
+ if (this.callStateSubject.value === 'talking') {
1561
+ this.callStateSubject.next('connected');
1562
+ this.statusTextSubject.next('Listening...');
1563
+ }
1564
+ };
1565
+ VoiceAgentService.prototype.setupBotAudioTrack = function (track) {
1566
+ var _a;
1567
+ if (!this.botAudioElement) {
1568
+ this.botAudioElement = new Audio();
1569
+ this.botAudioElement.autoplay = true;
1570
+ }
1571
+ var existing = (_a = this.botAudioElement.srcObject) === null || _a === void 0 ? void 0 : _a.getAudioTracks()[0];
1572
+ if ((existing === null || existing === void 0 ? void 0 : existing.id) === track.id)
1573
+ return;
1574
+ this.botAudioElement.srcObject = new MediaStream([track]);
1575
+ this.botAudioElement.play().catch(function (err) { return console.warn('[HiveGpt Voice] Bot audio play blocked', err); });
1576
+ };
1577
+ VoiceAgentService.prototype.stopBotAudio = function () {
1578
+ var _a;
1579
+ if (this.botAudioElement) {
1580
+ try {
1581
+ this.botAudioElement.pause();
1582
+ (_a = this.botAudioElement.srcObject) === null || _a === void 0 ? void 0 : _a.getAudioTracks().forEach(function (t) { return t.stop(); });
1583
+ this.botAudioElement.srcObject = null;
1790
1584
  }
1791
- else if (current === 'talking' || current === 'listening') {
1792
- _this.callStateSubject.next('connected');
1585
+ catch (_b) {
1586
+ // ignore
1793
1587
  }
1794
- }));
1588
+ this.botAudioElement = null;
1589
+ }
1795
1590
  };
1796
- VoiceAgentService.prototype.startLocalMic = function () {
1591
+ VoiceAgentService.prototype.disconnect = function () {
1797
1592
  return __awaiter(this, void 0, void 0, function () {
1798
- var stream, track;
1799
- return __generator(this, function (_b) {
1800
- switch (_b.label) {
1593
+ return __generator(this, function (_d) {
1594
+ switch (_d.label) {
1801
1595
  case 0:
1802
- this.stopLocalMic();
1803
- return [4 /*yield*/, navigator.mediaDevices.getUserMedia({ audio: true })];
1596
+ this.stopDurationTimer();
1597
+ this.callStartTime = 0;
1598
+ this.audioAnalyzer.stop();
1599
+ this.stopBotAudio();
1600
+ return [4 /*yield*/, this.cleanupPipecatClient()];
1804
1601
  case 1:
1805
- stream = _b.sent();
1806
- track = stream.getAudioTracks()[0];
1807
- if (!track) {
1808
- stream.getTracks().forEach(function (t) { return t.stop(); });
1809
- throw new Error('No audio track');
1810
- }
1811
- this.localMicStream = stream;
1812
- this.isMicMutedSubject.next(!track.enabled);
1813
- this.audioAnalyzer.start(stream);
1602
+ _d.sent();
1603
+ this.callStateSubject.next('ended');
1604
+ this.statusTextSubject.next('Call Ended');
1814
1605
  return [2 /*return*/];
1815
1606
  }
1816
1607
  });
1817
1608
  });
1818
1609
  };
1819
- VoiceAgentService.prototype.stopLocalMic = function () {
1820
- if (this.localMicStream) {
1821
- this.localMicStream.getTracks().forEach(function (t) { return t.stop(); });
1822
- this.localMicStream = null;
1823
- }
1824
- };
1825
- VoiceAgentService.prototype.enqueueRemoteAudio = function (chunk) {
1826
- this.pendingRemoteAudio.push(chunk.slice(0));
1827
- if (!this.remoteAudioPlaying) {
1828
- void this.playRemoteAudioQueue();
1829
- }
1830
- };
1831
- VoiceAgentService.prototype.playRemoteAudioQueue = function () {
1610
+ VoiceAgentService.prototype.cleanupPipecatClient = function () {
1832
1611
  return __awaiter(this, void 0, void 0, function () {
1833
- var context, chunk, decoded, _a_1;
1834
- return __generator(this, function (_b) {
1835
- switch (_b.label) {
1612
+ var _a_1;
1613
+ return __generator(this, function (_d) {
1614
+ switch (_d.label) {
1836
1615
  case 0:
1837
- this.remoteAudioPlaying = true;
1838
- context = this.getOrCreateRemoteAudioContext();
1839
- _b.label = 1;
1616
+ if (!this.pcClient) return [3 /*break*/, 5];
1617
+ _d.label = 1;
1840
1618
  case 1:
1841
- if (!(this.pendingRemoteAudio.length > 0)) return [3 /*break*/, 7];
1842
- chunk = this.pendingRemoteAudio.shift();
1843
- if (!chunk)
1844
- return [3 /*break*/, 1];
1845
- _b.label = 2;
1619
+ _d.trys.push([1, 3, , 4]);
1620
+ return [4 /*yield*/, this.pcClient.disconnect()];
1846
1621
  case 2:
1847
- _b.trys.push([2, 5, , 6]);
1848
- return [4 /*yield*/, this.decodeAudioChunk(context, chunk)];
1622
+ _d.sent();
1623
+ return [3 /*break*/, 4];
1849
1624
  case 3:
1850
- decoded = _b.sent();
1851
- this.assistantAudioStarted();
1852
- return [4 /*yield*/, this.playDecodedBuffer(context, decoded)];
1625
+ _a_1 = _d.sent();
1626
+ return [3 /*break*/, 4];
1853
1627
  case 4:
1854
- _b.sent();
1855
- return [3 /*break*/, 6];
1856
- case 5:
1857
- _a_1 = _b.sent();
1858
- return [3 /*break*/, 6];
1859
- case 6: return [3 /*break*/, 1];
1860
- case 7:
1861
- this.remoteAudioPlaying = false;
1862
- this.assistantAudioStopped();
1863
- return [2 /*return*/];
1628
+ this.pcClient = null;
1629
+ _d.label = 5;
1630
+ case 5: return [2 /*return*/];
1864
1631
  }
1865
1632
  });
1866
1633
  });
1867
1634
  };
1868
- VoiceAgentService.prototype.getOrCreateRemoteAudioContext = function () {
1869
- if (!this.remoteAudioContext || this.remoteAudioContext.state === 'closed') {
1870
- this.remoteAudioContext = new AudioContext();
1871
- }
1872
- if (this.remoteAudioContext.state === 'suspended') {
1873
- void this.remoteAudioContext.resume();
1874
- }
1875
- return this.remoteAudioContext;
1876
- };
1877
- VoiceAgentService.prototype.decodeAudioChunk = function (context, chunk) {
1878
- return new Promise(function (resolve, reject) {
1879
- context.decodeAudioData(chunk.slice(0), resolve, reject);
1880
- });
1881
- };
1882
- VoiceAgentService.prototype.playDecodedBuffer = function (context, buffer) {
1883
- return new Promise(function (resolve) {
1884
- var source = context.createBufferSource();
1885
- source.buffer = buffer;
1886
- source.connect(context.destination);
1887
- source.onended = function () { return resolve(); };
1888
- source.start();
1889
- });
1890
- };
1891
- VoiceAgentService.prototype.assistantAudioStarted = function () {
1892
- if (this.callStartTime === 0) {
1893
- this.callStartTime = Date.now();
1894
- this.startDurationTimer();
1895
- }
1896
- this.callStateSubject.next('talking');
1897
- };
1898
- VoiceAgentService.prototype.assistantAudioStopped = function () {
1899
- if (this.callStateSubject.value === 'talking') {
1900
- this.callStateSubject.next('connected');
1901
- }
1902
- };
1903
- VoiceAgentService.prototype.resetRemoteAudioPlayback = function () {
1904
- this.pendingRemoteAudio = [];
1905
- this.remoteAudioPlaying = false;
1906
- if (this.remoteAudioContext && this.remoteAudioContext.state !== 'closed') {
1907
- this.remoteAudioContext.close().catch(function () { });
1908
- }
1909
- this.remoteAudioContext = null;
1910
- };
1911
- VoiceAgentService.prototype.handleRemoteClose = function () {
1912
- return __awaiter(this, void 0, void 0, function () {
1913
- var state;
1914
- return __generator(this, function (_b) {
1915
- state = this.callStateSubject.value;
1916
- if (state === 'idle' || state === 'ended')
1917
- return [2 /*return*/];
1918
- this.endCall$.next();
1919
- this.stopDurationTimer();
1920
- this.callStartTime = 0;
1921
- this.audioAnalyzer.stop();
1922
- this.stopLocalMic();
1923
- this.resetRemoteAudioPlayback();
1924
- this.callStateSubject.next('ended');
1925
- this.statusTextSubject.next('Connection lost');
1926
- return [2 /*return*/];
1927
- });
1928
- });
1929
- };
1930
- VoiceAgentService.prototype.disconnect = function () {
1931
- return __awaiter(this, void 0, void 0, function () {
1932
- return __generator(this, function (_b) {
1933
- this.endCall$.next();
1934
- this.stopDurationTimer();
1935
- this.callStartTime = 0;
1936
- this.audioAnalyzer.stop();
1937
- this.stopLocalMic();
1938
- this.resetRemoteAudioPlayback();
1939
- this.wsClient.disconnect();
1940
- this.callStateSubject.next('ended');
1941
- this.statusTextSubject.next('Call Ended');
1942
- return [2 /*return*/];
1943
- });
1944
- });
1945
- };
1946
1635
  VoiceAgentService.prototype.toggleMic = function () {
1947
- var _a;
1636
+ if (!this.pcClient)
1637
+ return;
1948
1638
  var nextMuted = !this.isMicMutedSubject.value;
1949
- var track = (_a = this.localMicStream) === null || _a === void 0 ? void 0 : _a.getAudioTracks()[0];
1950
- if (track) {
1951
- track.enabled = !nextMuted;
1952
- }
1639
+ this.pcClient.enableMic(!nextMuted);
1953
1640
  this.isMicMutedSubject.next(nextMuted);
1641
+ if (nextMuted)
1642
+ this.isUserSpeakingSubject.next(false);
1954
1643
  };
1955
1644
  VoiceAgentService.prototype.startDurationTimer = function () {
1956
1645
  var _this = this;
1957
- var updateDuration = function () {
1646
+ var tick = function () {
1958
1647
  if (_this.callStartTime > 0) {
1959
1648
  var elapsed = Math.floor((Date.now() - _this.callStartTime) / 1000);
1960
- var minutes = Math.floor(elapsed / 60);
1961
- var seconds = elapsed % 60;
1962
- _this.durationSubject.next(minutes + ":" + String(seconds).padStart(2, '0'));
1649
+ var m = Math.floor(elapsed / 60);
1650
+ var s = elapsed % 60;
1651
+ _this.durationSubject.next(m + ":" + String(s).padStart(2, '0'));
1963
1652
  }
1964
1653
  };
1965
- updateDuration();
1966
- this.durationInterval = setInterval(updateDuration, 1000);
1654
+ tick();
1655
+ this.durationInterval = setInterval(tick, 1000);
1967
1656
  };
1968
1657
  VoiceAgentService.prototype.stopDurationTimer = function () {
1969
1658
  if (this.durationInterval) {
@@ -1973,7 +1662,7 @@
1973
1662
  };
1974
1663
  return VoiceAgentService;
1975
1664
  }());
1976
- VoiceAgentService.ɵprov = i0__namespace.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0__namespace.ɵɵinject(AudioAnalyzerService), i0__namespace.ɵɵinject(WebSocketVoiceClientService), i0__namespace.ɵɵinject(PlatformTokenRefreshService), i0__namespace.ɵɵinject(i0__namespace.PLATFORM_ID)); }, token: VoiceAgentService, providedIn: "root" });
1665
+ VoiceAgentService.ɵprov = i0__namespace.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0__namespace.ɵɵinject(AudioAnalyzerService), i0__namespace.ɵɵinject(PlatformTokenRefreshService), i0__namespace.ɵɵinject(i0__namespace.NgZone), i0__namespace.ɵɵinject(i0__namespace.PLATFORM_ID)); }, token: VoiceAgentService, providedIn: "root" });
1977
1666
  VoiceAgentService.decorators = [
1978
1667
  { type: i0.Injectable, args: [{
1979
1668
  providedIn: 'root',
@@ -1981,8 +1670,8 @@
1981
1670
  ];
1982
1671
  VoiceAgentService.ctorParameters = function () { return [
1983
1672
  { type: AudioAnalyzerService },
1984
- { type: WebSocketVoiceClientService },
1985
1673
  { type: PlatformTokenRefreshService },
1674
+ { type: i0.NgZone },
1986
1675
  { type: Object, decorators: [{ type: i0.Inject, args: [i0.PLATFORM_ID,] }] }
1987
1676
  ]; };
1988
1677
 
@@ -2013,12 +1702,27 @@
2013
1702
  this.isMicMuted = false;
2014
1703
  this.isUserSpeaking = false;
2015
1704
  this.audioLevels = [];
2016
- this.isSpeaking = false;
2017
- /** Track whether call has transitioned out of initial connected state. */
2018
- this.hasLeftConnectedOnce = false;
2019
1705
  this.subscriptions = [];
2020
1706
  this.isConnecting = false;
2021
1707
  }
1708
+ Object.defineProperty(VoiceAgentModalComponent.prototype, "isBotTalking", {
1709
+ /** True while the bot is speaking — drives avatar pulse animation and voice visualizer. */
1710
+ get: function () { return this.callState === 'talking'; },
1711
+ enumerable: false,
1712
+ configurable: true
1713
+ });
1714
+ Object.defineProperty(VoiceAgentModalComponent.prototype, "isUserActive", {
1715
+ /** True while the user is actively speaking — drives waveform active color. */
1716
+ get: function () { return this.callState === 'listening' && this.isUserSpeaking && !this.isMicMuted; },
1717
+ enumerable: false,
1718
+ configurable: true
1719
+ });
1720
+ Object.defineProperty(VoiceAgentModalComponent.prototype, "isProcessing", {
1721
+ /** True during the brief processing pause between user speech and bot response. */
1722
+ get: function () { return this.callState === 'connected' && this.statusText === 'Processing...'; },
1723
+ enumerable: false,
1724
+ configurable: true
1725
+ });
2022
1726
  VoiceAgentModalComponent.prototype.ngOnInit = function () {
2023
1727
  var _this = this;
2024
1728
  var _a, _b, _c, _d, _e, _f, _g, _h;
@@ -2040,16 +1744,8 @@
2040
1744
  this.agentAvatar = this.injectedConfig.agentAvatar;
2041
1745
  this.usersApiUrl = (_h = this.injectedConfig.usersApiUrl) !== null && _h !== void 0 ? _h : this.usersApiUrl;
2042
1746
  }
2043
- // Subscribe to observables
2044
1747
  this.subscriptions.push(this.voiceAgentService.callState$.subscribe(function (state) {
2045
1748
  _this.callState = state;
2046
- _this.isSpeaking = state === 'talking';
2047
- if (state === 'listening' || state === 'talking') {
2048
- _this.hasLeftConnectedOnce = true;
2049
- }
2050
- if (state === 'idle' || state === 'ended') {
2051
- _this.hasLeftConnectedOnce = false;
2052
- }
2053
1749
  }));
2054
1750
  this.subscriptions.push(this.voiceAgentService.statusText$.subscribe(function (text) {
2055
1751
  _this.statusText = text;
@@ -2134,18 +1830,16 @@
2134
1830
  return minHeight + (n / 100) * (maxHeight - minHeight);
2135
1831
  };
2136
1832
  Object.defineProperty(VoiceAgentModalComponent.prototype, "statusLabel", {
2137
- /** Status label for active call. */
1833
+ /** Status label for active call — driven by callState + service statusText. */
2138
1834
  get: function () {
2139
- if (this.callState === 'connecting')
2140
- return this.statusText || 'Connecting...';
2141
- if (this.callState === 'talking')
2142
- return 'Talking...';
2143
- if (this.callState === 'listening')
2144
- return 'Listening';
2145
- if (this.callState === 'connected') {
2146
- return this.hasLeftConnectedOnce ? 'Talking...' : 'Connected';
1835
+ switch (this.callState) {
1836
+ case 'connecting': return 'Connecting...';
1837
+ case 'talking': return 'Talking...';
1838
+ case 'listening': return 'Listening...';
1839
+ // 'connected' covers: initial connect, between turns (Listening / Processing)
1840
+ case 'connected': return this.statusText || 'Connected';
1841
+ default: return this.statusText || '';
2147
1842
  }
2148
- return this.statusText || '';
2149
1843
  },
2150
1844
  enumerable: false,
2151
1845
  configurable: true
@@ -2174,8 +1868,8 @@
2174
1868
  VoiceAgentModalComponent.decorators = [
2175
1869
  { type: i0.Component, args: [{
2176
1870
  selector: 'hivegpt-voice-agent-modal',
2177
- 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",
2178
- 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}"]
1871
+ 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",
1872
+ 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}"]
2179
1873
  },] }
2180
1874
  ];
2181
1875
  VoiceAgentModalComponent.ctorParameters = function () { return [
@@ -6163,8 +5857,8 @@
6163
5857
  };
6164
5858
 
6165
5859
  /**
6166
- * Voice agent module. Uses native WebSocket for the voice session.
6167
- * Does NOT use Socket.IO or ngx-socket-io.
5860
+ * Voice agent module. Uses @pipecat-ai/client-js + @pipecat-ai/websocket-transport
5861
+ * (peer dependencies) for WebSocket transport, RTVI protocol, and audio.
6168
5862
  */
6169
5863
  var VoiceAgentModule = /** @class */ (function () {
6170
5864
  function VoiceAgentModule() {
@@ -6182,7 +5876,6 @@
6182
5876
  providers: [
6183
5877
  VoiceAgentService,
6184
5878
  AudioAnalyzerService,
6185
- WebSocketVoiceClientService
6186
5879
  ],
6187
5880
  exports: [
6188
5881
  VoiceAgentModalComponent
@@ -6483,10 +6176,9 @@
6483
6176
  exports["ɵc"] = ConversationService;
6484
6177
  exports["ɵd"] = NotificationSocket;
6485
6178
  exports["ɵe"] = TranslationService;
6486
- exports["ɵf"] = WebSocketVoiceClientService;
6487
- exports["ɵg"] = VideoPlayerComponent;
6488
- exports["ɵh"] = SafeHtmlPipe;
6489
- exports["ɵi"] = BotHtmlEditorComponent;
6179
+ exports["ɵf"] = VideoPlayerComponent;
6180
+ exports["ɵg"] = SafeHtmlPipe;
6181
+ exports["ɵh"] = BotHtmlEditorComponent;
6490
6182
 
6491
6183
  Object.defineProperty(exports, '__esModule', { value: true });
6492
6184