@crestapps/ai-chat-ui 2.0.0-preview.154 → 2.0.0-preview.162

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ai-chat.js CHANGED
@@ -668,6 +668,15 @@ window.coreAIChatManager = function () {
668
668
  ttsInstanceId: 'ai-chat-' + Math.random().toString(36).slice(2),
669
669
  singleResponseMode: !!config.singleResponseMode,
670
670
  conversationModeEnabled: config.chatMode === 'Conversation',
671
+ realtimeEnabled: config.chatMode === 'Realtime' || !!config.realtimeEnabled,
672
+ // Server-relay WebRTC transport: primary when advertised and supported; the client falls back
673
+ // to the WebSocket path below if the peer cannot connect.
674
+ realtimeWebRtcEnabled: config.realtimeWebRtcEnabled === true && typeof window.RTCPeerConnection === 'function',
675
+ // A host-supplied override. Normally the servers are fetched from the hub right before the
676
+ // peer is created (resolveRealtimeIceServers) so configured TURN relays — and their
677
+ // short-lived credentials — are actually used instead of a public STUN server.
678
+ realtimeWebRtcIceServers: Array.isArray(config.realtimeWebRtcIceServers) && config.realtimeWebRtcIceServers.length ? config.realtimeWebRtcIceServers : null,
679
+ realtimeVoiceName: config.realtimeVoiceName || null,
671
680
  conversationButton: null,
672
681
  isConversationMode: false,
673
682
  notificationDismissTimers: {},
@@ -1169,6 +1178,8 @@ window.coreAIChatManager = function () {
1169
1178
  _this3.connection.keepAliveIntervalInMilliseconds = 15000;
1170
1179
  _this3.connection.on("LoadSession", function (data) {
1171
1180
  var _data$messages;
1181
+ // Switching to another session must not leave a prior realtime/voice session running.
1182
+ _this3.forceStopActiveVoice();
1172
1183
  _this3.initializeSession(data.sessionId, true);
1173
1184
  _this3.messages = [];
1174
1185
  _this3.documents = data.documents || [];
@@ -1194,6 +1205,10 @@ window.coreAIChatManager = function () {
1194
1205
  });
1195
1206
  }
1196
1207
  });
1208
+
1209
+ // Realtime lifecycle events (ReceiveRealtimeEvent, and ReceiveError while a voice session is
1210
+ // live) are handled by the shared CoreAIRealtime module — see setupRealtimeController.
1211
+
1197
1212
  _this3.connection.on("ReceiveError", function (error) {
1198
1213
  console.error("SignalR Error: ", error);
1199
1214
  if (_this3.isRecording) {
@@ -1265,20 +1280,25 @@ window.coreAIChatManager = function () {
1265
1280
  }
1266
1281
  }
1267
1282
  });
1268
- _this3.connection.on("ReceiveConversationUserMessage", function (sessionId, text) {
1283
+ _this3.connection.on("ReceiveConversationUserMessage", function (sessionId, turnId, text) {
1284
+ // Realtime creates the chat session server-side; adopt its id (and update the page URL)
1285
+ // on the first turn so a refresh reloads the conversation instead of starting fresh.
1286
+ if (sessionId && _this3.getSessionId() !== sessionId) {
1287
+ _this3.initializeSession(sessionId);
1288
+ }
1289
+
1290
+ // Realtime: a placeholder for this turn was already inserted in the right position when
1291
+ // the utterance was captured (see user_turn_pending), so just fill in its text.
1292
+ if (turnId && _this3._realtimePendingTurns && _this3._realtimePendingTurns[turnId]) {
1293
+ var pending = _this3._realtimePendingTurns[turnId];
1294
+ delete _this3._realtimePendingTurns[turnId];
1295
+ _this3.stopAudio();
1296
+ _this3.fillRealtimePendingTurn(pending, text);
1297
+ return;
1298
+ }
1269
1299
  if (text) {
1270
1300
  _this3.stopAudio();
1271
1301
 
1272
- // If there's an interrupted assistant message still streaming,
1273
- // mark it as done to stop the spinner animation.
1274
- if (_this3._conversationAssistantMessage) {
1275
- var oldMsg = _this3.messages[_this3._conversationAssistantMessage.index];
1276
- if (oldMsg) {
1277
- oldMsg.isStreaming = false;
1278
- }
1279
- _this3._conversationAssistantMessage = null;
1280
- }
1281
-
1282
1302
  // Replace the partial transcript message with the final one.
1283
1303
  if (_this3._conversationPartialMessage) {
1284
1304
  var escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -1286,6 +1306,23 @@ window.coreAIChatManager = function () {
1286
1306
  _this3._conversationPartialMessage.htmlContent = '<p>' + escaped + '</p>';
1287
1307
  _this3._conversationPartialMessage.isPartial = false;
1288
1308
  _this3._conversationPartialMessage = null;
1309
+ } else if (_this3._conversationAssistantMessage) {
1310
+ // Realtime: the user's transcript lags the assistant's reply for the SAME turn (the
1311
+ // model answers the audio before speech-to-text finishes). Insert the user message
1312
+ // just before the streaming assistant message so the order stays You -> Assistant,
1313
+ // and keep that message streaming — ending it here split one reply into two bubbles
1314
+ // above the prompt.
1315
+ var at = _this3._conversationAssistantMessage.index;
1316
+ var userMsg = {
1317
+ role: 'user',
1318
+ content: text,
1319
+ rawContent: text,
1320
+ userRating: null,
1321
+ references: {}
1322
+ };
1323
+ updateMessagePresentation(userMsg, userMsg.references);
1324
+ _this3.messages.splice(at, 0, userMsg);
1325
+ _this3._conversationAssistantMessage.index = at + 1;
1289
1326
  } else {
1290
1327
  _this3.addMessage({
1291
1328
  role: 'user',
@@ -1296,6 +1333,15 @@ window.coreAIChatManager = function () {
1296
1333
  }
1297
1334
  });
1298
1335
  _this3.connection.on("ReceiveConversationAssistantToken", function (sessionId, messageId, token, responseId, references, appearance) {
1336
+ // A new response id means a new turn — finalize the previous assistant message so two replies
1337
+ // never merge into one bubble (also covers barge-in, which starts a fresh response).
1338
+ if (_this3._conversationAssistantMessage && _this3._conversationAssistantResponseId && responseId && _this3._conversationAssistantResponseId !== responseId) {
1339
+ var prev = _this3.messages[_this3._conversationAssistantMessage.index];
1340
+ if (prev) {
1341
+ prev.isStreaming = false;
1342
+ }
1343
+ _this3._conversationAssistantMessage = null;
1344
+ }
1299
1345
  if (!_this3._conversationAssistantMessage) {
1300
1346
  _this3.stopAudio();
1301
1347
  _this3.hideTypingIndicator();
@@ -1322,6 +1368,7 @@ window.coreAIChatManager = function () {
1322
1368
  index: msgIndex,
1323
1369
  content: ''
1324
1370
  };
1371
+ _this3._conversationAssistantResponseId = responseId;
1325
1372
  }
1326
1373
  _this3._conversationAssistantMessage.content += token;
1327
1374
  var msg = _this3.messages[_this3._conversationAssistantMessage.index];
@@ -1347,15 +1394,27 @@ window.coreAIChatManager = function () {
1347
1394
  updateMessagePresentation(msg, msg.references);
1348
1395
  }
1349
1396
  _this3._conversationAssistantMessage = null;
1397
+ _this3._conversationAssistantResponseId = null;
1350
1398
  }
1351
1399
  });
1352
1400
  _this3.connection.on("ReceiveAudioChunk", function (sessionId, base64Audio, contentType) {
1353
- if (base64Audio) {
1354
- var binaryString = atob(base64Audio);
1355
- var bytes = new Uint8Array(binaryString.length);
1356
- for (var i = 0; i < binaryString.length; i++) {
1357
- bytes[i] = binaryString.charCodeAt(i);
1401
+ if (!base64Audio) {
1402
+ return;
1403
+ }
1404
+ var binaryString = atob(base64Audio);
1405
+ var bytes = new Uint8Array(binaryString.length);
1406
+ for (var i = 0; i < binaryString.length; i++) {
1407
+ bytes[i] = binaryString.charCodeAt(i);
1408
+ }
1409
+
1410
+ // Realtime audio arrives as raw PCM16 (WebSocket transport) and is scheduled for immediate
1411
+ // playback by the shared module; conversation/TTS audio (mp3/wav) is collected and played on
1412
+ // complete.
1413
+ if (contentType === 'audio/pcm') {
1414
+ if (_this3.realtimeController) {
1415
+ _this3.realtimeController.receivePcm(bytes);
1358
1416
  }
1417
+ } else {
1359
1418
  _this3.audioChunks.push(bytes);
1360
1419
  }
1361
1420
  });
@@ -1373,6 +1432,11 @@ window.coreAIChatManager = function () {
1373
1432
  });
1374
1433
  _this3.connection.onreconnecting(function () {
1375
1434
  console.warn("SignalR: reconnecting...");
1435
+
1436
+ // The realtime session lived inside the dropped connection; it cannot survive a reconnect.
1437
+ if (_this3.realtimeEnabled && _this3.isConversationMode) {
1438
+ _this3.stopRealtimeConversation();
1439
+ }
1376
1440
  });
1377
1441
  _this3.connection.onreconnected(function () {
1378
1442
  console.info("SignalR: reconnected.");
@@ -1384,6 +1448,9 @@ window.coreAIChatManager = function () {
1384
1448
  if (_this3.isNavigatingAway) {
1385
1449
  return;
1386
1450
  }
1451
+ if (_this3.realtimeEnabled && _this3.isConversationMode) {
1452
+ _this3.stopRealtimeConversation();
1453
+ }
1387
1454
  if (error) {
1388
1455
  console.warn("SignalR connection closed with error:", error.message || error);
1389
1456
  }
@@ -1404,8 +1471,69 @@ window.coreAIChatManager = function () {
1404
1471
  }, _callee6, null, [[1, 3]]);
1405
1472
  }))();
1406
1473
  },
1407
- addMessageInternal: function addMessageInternal(message) {
1474
+ ensureConnectionStarted: function ensureConnectionStarted() {
1408
1475
  var _this4 = this;
1476
+ return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7() {
1477
+ var states, _t5;
1478
+ return _regenerator().w(function (_context7) {
1479
+ while (1) switch (_context7.p = _context7.n) {
1480
+ case 0:
1481
+ if (_this4.connection) {
1482
+ _context7.n = 1;
1483
+ break;
1484
+ }
1485
+ return _context7.a(2, false);
1486
+ case 1:
1487
+ states = signalR.HubConnectionState;
1488
+ if (!(_this4.connection.state === states.Connected)) {
1489
+ _context7.n = 2;
1490
+ break;
1491
+ }
1492
+ return _context7.a(2, true);
1493
+ case 2:
1494
+ if (!(_this4.connection.state === states.Disconnected)) {
1495
+ _context7.n = 7;
1496
+ break;
1497
+ }
1498
+ _context7.p = 3;
1499
+ _context7.n = 4;
1500
+ return _this4.connection.start();
1501
+ case 4:
1502
+ _context7.n = 6;
1503
+ break;
1504
+ case 5:
1505
+ _context7.p = 5;
1506
+ _t5 = _context7.v;
1507
+ console.error("SignalR Connection Error: ", _t5);
1508
+ case 6:
1509
+ return _context7.a(2, _this4.connection.state === states.Connected);
1510
+ case 7:
1511
+ _context7.n = 8;
1512
+ return new Promise(function (resolve) {
1513
+ var settle = function settle(value) {
1514
+ clearTimeout(timeoutId);
1515
+ clearInterval(intervalId);
1516
+ resolve(value);
1517
+ };
1518
+ var timeoutId = setTimeout(function () {
1519
+ return settle(_this4.connection.state === states.Connected);
1520
+ }, 10000);
1521
+ var intervalId = setInterval(function () {
1522
+ if (_this4.connection.state === states.Connected) {
1523
+ settle(true);
1524
+ } else if (_this4.connection.state === states.Disconnected) {
1525
+ settle(false);
1526
+ }
1527
+ }, 100);
1528
+ });
1529
+ case 8:
1530
+ return _context7.a(2, _context7.v);
1531
+ }
1532
+ }, _callee7, null, [[3, 5]]);
1533
+ }))();
1534
+ },
1535
+ addMessageInternal: function addMessageInternal(message) {
1536
+ var _this5 = this;
1409
1537
  if (message.role === 'assistant') {
1410
1538
  message.appearance = this.normalizeAssistantAppearance(message.appearance);
1411
1539
  }
@@ -1419,7 +1547,7 @@ window.coreAIChatManager = function () {
1419
1547
  }));
1420
1548
  this.messages.push(message);
1421
1549
  this.$nextTick(function () {
1422
- _this4.fireEvent(new CustomEvent("addedCoreAIPromotMessage", {
1550
+ _this5.fireEvent(new CustomEvent("addedCoreAIPromotMessage", {
1423
1551
  detail: {
1424
1552
  message: message
1425
1553
  }
@@ -1427,7 +1555,7 @@ window.coreAIChatManager = function () {
1427
1555
  });
1428
1556
  },
1429
1557
  addMessage: function addMessage(message) {
1430
- var _this5 = this;
1558
+ var _this6 = this;
1431
1559
  // Ensure userRating is always defined for Vue reactivity.
1432
1560
  if (message.userRating === undefined) {
1433
1561
  message.userRating = null;
@@ -1442,17 +1570,17 @@ window.coreAIChatManager = function () {
1442
1570
  this.$nextTick(function () {
1443
1571
  // Render any pending charts once the DOM is updated
1444
1572
  renderChartsInMessage(message);
1445
- _this5.scrollToBottom();
1573
+ _this6.scrollToBottom();
1446
1574
  });
1447
1575
  },
1448
1576
  addMessages: function addMessages(messages) {
1449
- var _this6 = this;
1577
+ var _this7 = this;
1450
1578
  for (var i = 0; i < messages.length; i++) {
1451
1579
  this.addMessageInternal(messages[i]);
1452
1580
  }
1453
1581
  this.hidePlaceholder();
1454
1582
  this.$nextTick(function () {
1455
- _this6.scrollToBottom();
1583
+ _this7.scrollToBottom();
1456
1584
  });
1457
1585
  },
1458
1586
  hidePlaceholder: function hidePlaceholder() {
@@ -1529,7 +1657,7 @@ window.coreAIChatManager = function () {
1529
1657
  this.prompt = '';
1530
1658
  },
1531
1659
  startRecording: function startRecording() {
1532
- var _this7 = this;
1660
+ var _this8 = this;
1533
1661
  if (this.isRecording || !this.connection) {
1534
1662
  return;
1535
1663
  }
@@ -1540,66 +1668,147 @@ window.coreAIChatManager = function () {
1540
1668
  autoGainControl: true
1541
1669
  }
1542
1670
  }).then(function (stream) {
1543
- var mimeType = MediaRecorder.isTypeSupported('audio/ogg;codecs=opus') ? 'audio/ogg;codecs=opus' : MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
1544
- _this7.mediaRecorder = new MediaRecorder(stream, {
1545
- mimeType: mimeType,
1546
- audioBitsPerSecond: 128000
1547
- });
1548
- _this7.preRecordingPrompt = _this7.prompt;
1549
- _this7._audioInputSent = false;
1671
+ _this8.preRecordingPrompt = _this8.prompt;
1672
+ _this8._audioInputSent = false;
1550
1673
  var subject = new signalR.Subject();
1551
- var profileId = _this7.getProfileId();
1552
- var sessionId = _this7.getSessionId() || '';
1553
- var pendingChunk = Promise.resolve();
1554
- _this7.mediaRecorder.addEventListener('dataavailable', function (e) {
1555
- if (e.data && e.data.size > 0) {
1556
- pendingChunk = pendingChunk.then(/*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7() {
1557
- var data, uint8Array, binaryString, base64;
1558
- return _regenerator().w(function (_context7) {
1559
- while (1) switch (_context7.n) {
1560
- case 0:
1561
- _context7.n = 1;
1562
- return e.data.arrayBuffer();
1563
- case 1:
1564
- data = _context7.v;
1565
- uint8Array = new Uint8Array(data);
1566
- binaryString = uint8Array.reduce(function (str, _byte) {
1567
- return str + String.fromCharCode(_byte);
1568
- }, '');
1569
- base64 = btoa(binaryString);
1570
- subject.next(base64);
1571
- case 2:
1572
- return _context7.a(2);
1573
- }
1574
- }, _callee7);
1575
- })));
1576
- }
1577
- });
1578
- _this7.mediaRecorder.addEventListener('stop', function () {
1579
- stream.getTracks().forEach(function (track) {
1580
- return track.stop();
1581
- });
1582
- pendingChunk.then(function () {
1583
- return subject.complete();
1584
- });
1674
+ var profileId = _this8.getProfileId();
1675
+ var sessionId = _this8.getSessionId() || '';
1676
+
1677
+ // Capture raw 16 kHz PCM (16-bit mono) via Web Audio instead of MediaRecorder.
1678
+ // Browsers only agree on WebM/Opus for MediaRecorder, and Azure's speech SDK cannot
1679
+ // demux that container from a streaming push (it fails inside GStreamer). Raw PCM is
1680
+ // decoded natively with no GStreamer and is produced identically by every browser.
1681
+ _this8._sttCapture = _this8._createPcmCapture(stream, function (base64) {
1682
+ try {
1683
+ subject.next(base64);
1684
+ } catch (err) {/* completed */}
1585
1685
  });
1686
+ _this8._sttSubject = subject;
1687
+ var rate = _this8._sttCapture && _this8._sttCapture.sampleRate || 16000;
1586
1688
  var language = navigator.language || document.documentElement.lang || 'en-US';
1587
- _this7.connection.send("SendAudioStream", profileId, sessionId, subject, mimeType, language);
1588
- _this7.mediaRecorder.start(250);
1589
- _this7.isRecording = true;
1590
- _this7.updateMicButton();
1689
+ _this8.connection.send("SendAudioStream", profileId, sessionId, subject, "audio/pcm;rate=" + rate, language);
1690
+ _this8.isRecording = true;
1691
+ _this8.updateMicButton();
1591
1692
  })["catch"](function (err) {
1592
1693
  console.error('Microphone access denied:', err);
1593
1694
  });
1594
1695
  },
1595
1696
  stopRecording: function stopRecording() {
1596
- if (!this.isRecording || !this.mediaRecorder) {
1697
+ if (!this.isRecording) {
1597
1698
  return;
1598
1699
  }
1599
- this.mediaRecorder.stop();
1700
+ this._stopPcmCapture(this._sttCapture);
1701
+ this._sttCapture = null;
1702
+ if (this._sttSubject) {
1703
+ try {
1704
+ this._sttSubject.complete();
1705
+ } catch (err) {/* already completed */}
1706
+ this._sttSubject = null;
1707
+ }
1600
1708
  this.isRecording = false;
1601
1709
  this.updateMicButton();
1602
1710
  },
1711
+ // Captures microphone audio as raw 16 kHz, 16-bit mono PCM using Web Audio and invokes
1712
+ // onChunk(base64Pcm, rms) for each block. Returns a handle for _stopPcmCapture. This replaces
1713
+ // MediaRecorder so every browser streams a format Azure decodes without GStreamer.
1714
+ _createPcmCapture: function _createPcmCapture(stream, onChunk) {
1715
+ var AudioCtx = window.AudioContext || window.webkitAudioContext;
1716
+ // Ask the browser to resample the mic to 16 kHz; fall back to manual resampling if it will not.
1717
+ var audioContext;
1718
+ try {
1719
+ audioContext = new AudioCtx({
1720
+ sampleRate: 16000
1721
+ });
1722
+ } catch (e) {
1723
+ audioContext = new AudioCtx();
1724
+ }
1725
+ var srcRate = audioContext.sampleRate;
1726
+ var source = audioContext.createMediaStreamSource(stream);
1727
+ var processor = audioContext.createScriptProcessor(4096, 1, 1);
1728
+ var self = this;
1729
+ processor.onaudioprocess = function (e) {
1730
+ var input = e.inputBuffer.getChannelData(0);
1731
+ var pcm16 = self._downsampleToInt16(input, srcRate, 16000);
1732
+ if (!pcm16 || pcm16.length === 0) {
1733
+ return;
1734
+ }
1735
+ var sum = 0;
1736
+ for (var i = 0; i < pcm16.length; i++) {
1737
+ var v = pcm16[i] / 32768;
1738
+ sum += v * v;
1739
+ }
1740
+ var rms = Math.sqrt(sum / pcm16.length);
1741
+ var bytes = new Uint8Array(pcm16.buffer);
1742
+ var binary = '';
1743
+ for (var j = 0; j < bytes.length; j++) {
1744
+ binary += String.fromCharCode(bytes[j]);
1745
+ }
1746
+ onChunk(btoa(binary), rms);
1747
+ };
1748
+ source.connect(processor);
1749
+ // Some browsers only fire onaudioprocess while the node is connected to a destination.
1750
+ processor.connect(audioContext.destination);
1751
+ return {
1752
+ audioContext: audioContext,
1753
+ source: source,
1754
+ processor: processor,
1755
+ stream: stream,
1756
+ sampleRate: 16000
1757
+ };
1758
+ },
1759
+ _stopPcmCapture: function _stopPcmCapture(capture) {
1760
+ if (!capture) {
1761
+ return;
1762
+ }
1763
+ try {
1764
+ if (capture.processor) {
1765
+ capture.processor.onaudioprocess = null;
1766
+ capture.processor.disconnect();
1767
+ }
1768
+ } catch (e) {}
1769
+ try {
1770
+ if (capture.source) {
1771
+ capture.source.disconnect();
1772
+ }
1773
+ } catch (e) {}
1774
+ try {
1775
+ if (capture.stream) {
1776
+ capture.stream.getTracks().forEach(function (t) {
1777
+ t.stop();
1778
+ });
1779
+ }
1780
+ } catch (e) {}
1781
+ try {
1782
+ if (capture.audioContext) {
1783
+ capture.audioContext.close();
1784
+ }
1785
+ } catch (e) {}
1786
+ },
1787
+ // Converts a Float32 sample block to 16-bit PCM, resampling from srcRate to dstRate when needed.
1788
+ _downsampleToInt16: function _downsampleToInt16(input, srcRate, dstRate) {
1789
+ var s, i;
1790
+ if (!(dstRate < srcRate)) {
1791
+ var same = new Int16Array(input.length);
1792
+ for (i = 0; i < input.length; i++) {
1793
+ s = Math.max(-1, Math.min(1, input[i]));
1794
+ same[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
1795
+ }
1796
+ return same;
1797
+ }
1798
+ var ratio = srcRate / dstRate;
1799
+ var newLen = Math.floor(input.length / ratio);
1800
+ var out = new Int16Array(newLen);
1801
+ for (var j = 0; j < newLen; j++) {
1802
+ var idx = j * ratio;
1803
+ var i0 = Math.floor(idx);
1804
+ var i1 = i0 + 1 < input.length ? i0 + 1 : input.length - 1;
1805
+ var frac = idx - i0;
1806
+ s = input[i0] * (1 - frac) + input[i1] * frac;
1807
+ s = Math.max(-1, Math.min(1, s));
1808
+ out[j] = s < 0 ? s * 0x8000 : s * 0x7FFF;
1809
+ }
1810
+ return out;
1811
+ },
1603
1812
  toggleRecording: function toggleRecording() {
1604
1813
  if (this.isRecording) {
1605
1814
  this.stopRecording();
@@ -1620,7 +1829,7 @@ window.coreAIChatManager = function () {
1620
1829
  }
1621
1830
  },
1622
1831
  streamMessage: function streamMessage(profileId, trimmedPrompt, sessionProfileId) {
1623
- var _this8 = this;
1832
+ var _this9 = this;
1624
1833
  if (this.stream) {
1625
1834
  this.stream.dispose();
1626
1835
  this.stream = null;
@@ -1637,14 +1846,14 @@ window.coreAIChatManager = function () {
1637
1846
  var currentSessionId = this.getSessionId();
1638
1847
  this.stream = this.connection.stream("SendMessage", profileId, trimmedPrompt, currentSessionId, sessionProfileId).subscribe({
1639
1848
  next: function next(chunk) {
1640
- var message = _this8.messages[messageIndex];
1849
+ var message = _this9.messages[messageIndex];
1641
1850
  if (!message) {
1642
1851
  if (chunk.sessionId && !currentSessionId) {
1643
- _this8.initializeSession(chunk.sessionId);
1852
+ _this9.initializeSession(chunk.sessionId);
1644
1853
  }
1645
- _this8.hideTypingIndicator();
1854
+ _this9.hideTypingIndicator();
1646
1855
  // Re-assign the index after hiding the typing indicator.
1647
- messageIndex = _this8.messages.length;
1856
+ messageIndex = _this9.messages.length;
1648
1857
  var newMessage = {
1649
1858
  id: chunk.messageId,
1650
1859
  role: "assistant",
@@ -1654,7 +1863,7 @@ window.coreAIChatManager = function () {
1654
1863
  isStreaming: true,
1655
1864
  userRating: null
1656
1865
  };
1657
- _this8.messages.push(newMessage);
1866
+ _this9.messages.push(newMessage);
1658
1867
  message = newMessage;
1659
1868
  }
1660
1869
  if (chunk.title && (!message.title || message.title !== chunk.title)) {
@@ -1684,46 +1893,46 @@ window.coreAIChatManager = function () {
1684
1893
  // Update the existing message
1685
1894
  message.rawContent = content;
1686
1895
  updateMessagePresentation(message, references);
1687
- _this8.messages[messageIndex] = message;
1688
- _this8.$nextTick(function () {
1896
+ _this9.messages[messageIndex] = message;
1897
+ _this9.$nextTick(function () {
1689
1898
  renderChartsInMessage(message);
1690
- _this8.scrollToBottom();
1899
+ _this9.scrollToBottom();
1691
1900
  });
1692
1901
  },
1693
1902
  complete: function complete() {
1694
- var _this8$stream;
1695
- _this8.processReferences(references, messageIndex);
1696
- _this8.streamingFinished();
1697
- var msg = _this8.messages[messageIndex];
1903
+ var _this9$stream;
1904
+ _this9.processReferences(references, messageIndex);
1905
+ _this9.streamingFinished();
1906
+ var msg = _this9.messages[messageIndex];
1698
1907
  if (msg) {
1699
1908
  msg.isStreaming = false;
1700
1909
  }
1701
1910
  if (!msg || !msg.content) {
1702
1911
  // No content received at all.
1703
- _this8.hideTypingIndicator();
1912
+ _this9.hideTypingIndicator();
1704
1913
  }
1705
1914
 
1706
1915
  // Trigger text-to-speech only in conversation mode.
1707
- if (_this8.isConversationMode && _this8.textToSpeechEnabled && msg && msg.content) {
1708
- _this8.synthesizeSpeech(msg.content);
1916
+ if (_this9.isConversationMode && _this9.textToSpeechEnabled && msg && msg.content) {
1917
+ _this9.synthesizeSpeech(msg.content);
1709
1918
  }
1710
- (_this8$stream = _this8.stream) === null || _this8$stream === void 0 || _this8$stream.dispose();
1711
- _this8.stream = null;
1919
+ (_this9$stream = _this9.stream) === null || _this9$stream === void 0 || _this9$stream.dispose();
1920
+ _this9.stream = null;
1712
1921
  },
1713
1922
  error: function error(err) {
1714
- var _this8$stream2;
1715
- _this8.processReferences(references, messageIndex);
1716
- _this8.streamingFinished();
1717
- var msg = _this8.messages[messageIndex];
1923
+ var _this9$stream2;
1924
+ _this9.processReferences(references, messageIndex);
1925
+ _this9.streamingFinished();
1926
+ var msg = _this9.messages[messageIndex];
1718
1927
  if (msg) {
1719
1928
  msg.isStreaming = false;
1720
1929
  }
1721
- _this8.hideTypingIndicator();
1722
- if (!_this8.isNavigatingAway) {
1723
- _this8.addMessage(_this8.getServiceDownMessage());
1930
+ _this9.hideTypingIndicator();
1931
+ if (!_this9.isNavigatingAway) {
1932
+ _this9.addMessage(_this9.getServiceDownMessage());
1724
1933
  }
1725
- (_this8$stream2 = _this8.stream) === null || _this8$stream2 === void 0 || _this8$stream2.dispose();
1726
- _this8.stream = null;
1934
+ (_this9$stream2 = _this9.stream) === null || _this9$stream2 === void 0 || _this9$stream2.dispose();
1935
+ _this9.stream = null;
1727
1936
  console.error("Stream error:", err);
1728
1937
  }
1729
1938
  });
@@ -1745,17 +1954,17 @@ window.coreAIChatManager = function () {
1745
1954
  };
1746
1955
  },
1747
1956
  processReferences: function processReferences(references, messageIndex) {
1748
- var _this9 = this;
1957
+ var _this0 = this;
1749
1958
  references = normalizeReferences(references);
1750
1959
  if (Object.keys(references).length) {
1751
- var _ref23, _message$rawContent2;
1960
+ var _ref22, _message$rawContent2;
1752
1961
  var message = this.messages[messageIndex];
1753
- message.rawContent = (_ref23 = (_message$rawContent2 = message.rawContent) !== null && _message$rawContent2 !== void 0 ? _message$rawContent2 : message.content) !== null && _ref23 !== void 0 ? _ref23 : '';
1962
+ message.rawContent = (_ref22 = (_message$rawContent2 = message.rawContent) !== null && _message$rawContent2 !== void 0 ? _message$rawContent2 : message.content) !== null && _ref22 !== void 0 ? _ref22 : '';
1754
1963
  updateMessagePresentation(message, references);
1755
1964
  this.messages[messageIndex] = message;
1756
1965
  this.$nextTick(function () {
1757
1966
  renderChartsInMessage(message);
1758
- _this9.scrollToBottom();
1967
+ _this0.scrollToBottom();
1759
1968
  });
1760
1969
  }
1761
1970
  },
@@ -1806,31 +2015,31 @@ window.coreAIChatManager = function () {
1806
2015
  this.stopAudio(false);
1807
2016
  },
1808
2017
  updateTtsPlaybackButtons: function updateTtsPlaybackButtons() {
1809
- var _this0 = this;
2018
+ var _this1 = this;
1810
2019
  if (!this.chatContainer) {
1811
2020
  return;
1812
2021
  }
1813
2022
  var buttons = this.chatContainer.querySelectorAll('[data-tts-message-index]');
1814
2023
  buttons.forEach(function (button) {
1815
2024
  var buttonIndex = Number(button.getAttribute('data-tts-message-index'));
1816
- var isPlaying = buttonIndex === _this0.ttsPlayingMessageIndex;
2025
+ var isPlaying = buttonIndex === _this1.ttsPlayingMessageIndex;
1817
2026
  button.classList.toggle('tts-playing', isPlaying);
1818
2027
  button.setAttribute('title', isPlaying ? 'Pause audio' : 'Read aloud');
1819
2028
  });
1820
2029
  },
1821
2030
  updateCopyButtons: function updateCopyButtons() {
1822
- var _this1 = this;
2031
+ var _this10 = this;
1823
2032
  if (!this.chatContainer) {
1824
2033
  return;
1825
2034
  }
1826
2035
  var buttons = this.chatContainer.querySelectorAll('[data-copy-message-index]');
1827
2036
  buttons.forEach(function (button) {
1828
2037
  var buttonIndex = Number(button.getAttribute('data-copy-message-index'));
1829
- var isCopied = buttonIndex === _this1.copiedMessageIndex;
2038
+ var isCopied = buttonIndex === _this10.copiedMessageIndex;
1830
2039
  var iconHtml = isCopied ? '<i class="fa-solid fa-check"></i>' : '<i class="fa-solid fa-copy"></i>';
1831
2040
  button.classList.toggle('text-success', isCopied);
1832
2041
  button.classList.toggle('text-secondary', !isCopied);
1833
- button.setAttribute('title', isCopied ? _this1.copiedTitle : _this1.copyTitle);
2042
+ button.setAttribute('title', isCopied ? _this10.copiedTitle : _this10.copyTitle);
1834
2043
  button.replaceChildren(DOMPurify.sanitize(iconHtml, {
1835
2044
  RETURN_DOM_FRAGMENT: true
1836
2045
  }));
@@ -1849,7 +2058,7 @@ window.coreAIChatManager = function () {
1849
2058
  }));
1850
2059
  },
1851
2060
  synthesizeSpeech: function synthesizeSpeech(text, cacheIndex) {
1852
- var _this10 = this;
2061
+ var _this11 = this;
1853
2062
  if (!this.textToSpeechEnabled || !text || !this.connection) {
1854
2063
  return;
1855
2064
  }
@@ -1858,16 +2067,16 @@ window.coreAIChatManager = function () {
1858
2067
  this._ttsCacheIndex = cacheIndex !== undefined ? cacheIndex : -1;
1859
2068
  this.connection.invoke("SynthesizeSpeech", this.getProfileId(), this.getSessionId(), text, this.ttsVoiceName)["catch"](function (err) {
1860
2069
  console.error("TTS synthesis error:", err);
1861
- _this10.isPlayingAudio = false;
1862
- _this10.ttsPlayingMessageIndex = -1;
1863
- _this10._ttsCacheIndex = -1;
1864
- _this10.$nextTick(function () {
1865
- return _this10.updateTtsPlaybackButtons();
2070
+ _this11.isPlayingAudio = false;
2071
+ _this11.ttsPlayingMessageIndex = -1;
2072
+ _this11._ttsCacheIndex = -1;
2073
+ _this11.$nextTick(function () {
2074
+ return _this11.updateTtsPlaybackButtons();
1866
2075
  });
1867
2076
  });
1868
2077
  },
1869
2078
  toggleMessageTts: function toggleMessageTts(message, index) {
1870
- var _this11 = this;
2079
+ var _this12 = this;
1871
2080
  if (this.ttsPlayingMessageIndex === index) {
1872
2081
  this.stopAudio();
1873
2082
  return;
@@ -1880,7 +2089,7 @@ window.coreAIChatManager = function () {
1880
2089
  }));
1881
2090
  this.ttsPlayingMessageIndex = index;
1882
2091
  this.$nextTick(function () {
1883
- return _this11.updateTtsPlaybackButtons();
2092
+ return _this12.updateTtsPlaybackButtons();
1884
2093
  });
1885
2094
  if (this.ttsAudioCache[index]) {
1886
2095
  this.playAudioBlob(this.ttsAudioCache[index]);
@@ -1889,13 +2098,13 @@ window.coreAIChatManager = function () {
1889
2098
  this.synthesizeSpeech(message.content, index);
1890
2099
  },
1891
2100
  playCollectedAudio: function playCollectedAudio() {
1892
- var _this12 = this;
2101
+ var _this13 = this;
1893
2102
  if (this.audioChunks.length === 0) {
1894
2103
  if (!this.currentAudioElement && this.audioPlayQueue.length === 0) {
1895
2104
  this.isPlayingAudio = false;
1896
2105
  this.ttsPlayingMessageIndex = -1;
1897
2106
  this.$nextTick(function () {
1898
- return _this12.updateTtsPlaybackButtons();
2107
+ return _this13.updateTtsPlaybackButtons();
1899
2108
  });
1900
2109
  }
1901
2110
  return;
@@ -1935,39 +2144,39 @@ window.coreAIChatManager = function () {
1935
2144
  this.playAudioBlob(blob);
1936
2145
  },
1937
2146
  playAudioBlob: function playAudioBlob(blob) {
1938
- var _this13 = this;
2147
+ var _this14 = this;
1939
2148
  var url = URL.createObjectURL(blob);
1940
2149
  var audio = new Audio(url);
1941
2150
  this.currentAudioUrl = url;
1942
2151
  this.currentAudioElement = audio;
1943
2152
  this.isPlayingAudio = true;
1944
2153
  audio.addEventListener('ended', function () {
1945
- _this13.currentAudioElement = null;
1946
- _this13.currentAudioUrl = null;
2154
+ _this14.currentAudioElement = null;
2155
+ _this14.currentAudioUrl = null;
1947
2156
  URL.revokeObjectURL(url);
1948
- _this13.playNextInQueue();
2157
+ _this14.playNextInQueue();
1949
2158
  });
1950
2159
  audio.addEventListener('error', function () {
1951
- _this13.currentAudioElement = null;
1952
- _this13.currentAudioUrl = null;
2160
+ _this14.currentAudioElement = null;
2161
+ _this14.currentAudioUrl = null;
1953
2162
  URL.revokeObjectURL(url);
1954
- _this13.playNextInQueue();
2163
+ _this14.playNextInQueue();
1955
2164
  });
1956
2165
  audio.play()["catch"](function (err) {
1957
2166
  console.error("Audio playback error:", err);
1958
- _this13.currentAudioElement = null;
1959
- _this13.currentAudioUrl = null;
2167
+ _this14.currentAudioElement = null;
2168
+ _this14.currentAudioUrl = null;
1960
2169
  URL.revokeObjectURL(url);
1961
- _this13.audioPlayQueue = [];
1962
- _this13.isPlayingAudio = false;
1963
- _this13.ttsPlayingMessageIndex = -1;
1964
- _this13.$nextTick(function () {
1965
- return _this13.updateTtsPlaybackButtons();
2170
+ _this14.audioPlayQueue = [];
2171
+ _this14.isPlayingAudio = false;
2172
+ _this14.ttsPlayingMessageIndex = -1;
2173
+ _this14.$nextTick(function () {
2174
+ return _this14.updateTtsPlaybackButtons();
1966
2175
  });
1967
2176
  });
1968
2177
  },
1969
2178
  playNextInQueue: function playNextInQueue() {
1970
- var _this14 = this;
2179
+ var _this15 = this;
1971
2180
  if (this.audioPlayQueue.length > 0) {
1972
2181
  var nextBlob = this.audioPlayQueue.shift();
1973
2182
  this.playAudioBlob(nextBlob);
@@ -1975,13 +2184,13 @@ window.coreAIChatManager = function () {
1975
2184
  this.isPlayingAudio = false;
1976
2185
  this.ttsPlayingMessageIndex = -1;
1977
2186
  this.$nextTick(function () {
1978
- return _this14.updateTtsPlaybackButtons();
2187
+ return _this15.updateTtsPlaybackButtons();
1979
2188
  });
1980
2189
  this.conversationModeOnAudioEnded();
1981
2190
  }
1982
2191
  },
1983
2192
  stopAudio: function stopAudio() {
1984
- var _this15 = this;
2193
+ var _this16 = this;
1985
2194
  if (this.currentAudioElement) {
1986
2195
  this.currentAudioElement.pause();
1987
2196
  this.currentAudioElement.currentTime = 0;
@@ -1996,18 +2205,177 @@ window.coreAIChatManager = function () {
1996
2205
  this.isPlayingAudio = false;
1997
2206
  this.ttsPlayingMessageIndex = -1;
1998
2207
  this.$nextTick(function () {
1999
- return _this15.updateTtsPlaybackButtons();
2208
+ return _this16.updateTtsPlaybackButtons();
2000
2209
  });
2001
2210
  },
2002
2211
  toggleConversationMode: function toggleConversationMode() {
2212
+ if (this.realtimeEnabled) {
2213
+ // Realtime is driven by the shared module, which owns the button; this only exists for
2214
+ // callers that toggle programmatically.
2215
+ if (this.realtimeController) {
2216
+ this.realtimeController.toggle();
2217
+ }
2218
+ return;
2219
+ }
2003
2220
  if (this.isConversationMode) {
2004
2221
  this.stopConversationMode();
2005
2222
  } else {
2006
2223
  this.startConversationMode();
2007
2224
  }
2008
2225
  },
2226
+ // Force-stop any in-progress voice/realtime session (mic, playback, and the streamed audio),
2227
+ // e.g. before starting a new chat or switching sessions, so it never keeps running in the background.
2228
+ forceStopActiveVoice: function forceStopActiveVoice() {
2229
+ if (!this.isConversationMode) {
2230
+ return;
2231
+ }
2232
+ if (this.realtimeEnabled) {
2233
+ this.stopRealtimeConversation();
2234
+ } else {
2235
+ this.stopConversationMode();
2236
+ }
2237
+ },
2238
+ startRealtimeConversation: function startRealtimeConversation() {
2239
+ if (this.realtimeController) {
2240
+ this.realtimeController.start();
2241
+ }
2242
+ },
2243
+ stopRealtimeConversation: function stopRealtimeConversation() {
2244
+ if (this.realtimeController) {
2245
+ this.realtimeController.stop();
2246
+ }
2247
+ },
2248
+ // Realtime (speech-to-speech) is the shared CoreAIRealtime module (realtime-audio.js): microphone
2249
+ // capture and gating, the WebRTC transport with its WebSocket fallback, playback, the settings
2250
+ // popover, push-to-talk, and the server-driven session lifecycle all live there, so this app and the
2251
+ // chat-interaction client cannot drift apart. What is wired here is only how the module reaches this
2252
+ // app's hub and transcript.
2253
+ setupRealtimeController: function setupRealtimeController(config) {
2254
+ if (this.realtimeController || !this.conversationButton) {
2255
+ return;
2256
+ }
2257
+ if (!window.CoreAIRealtime || typeof window.CoreAIRealtime.attach !== 'function') {
2258
+ console.error('realtime-audio.js is not loaded; realtime voice is unavailable on this page.');
2259
+ return;
2260
+ }
2261
+ var self = this;
2262
+ this.realtimeController = window.CoreAIRealtime.attach({
2263
+ connection: this.connection,
2264
+ ensureConnected: function ensureConnected() {
2265
+ return self.ensureConnectionStarted().then(function (connected) {
2266
+ if (!connected) {
2267
+ throw new Error('The chat connection is not available.');
2268
+ }
2269
+ return connected;
2270
+ });
2271
+ },
2272
+ sendStart: function sendStart(subject, voice, language, silenceMs, vadThreshold, allowInterruption) {
2273
+ self.connection.send('StartRealtimeConversation', self.getProfileId(), self.getSessionId() || '', subject, voice, language, silenceMs, vadThreshold, allowInterruption);
2274
+ },
2275
+ webRtcEnabled: this.realtimeWebRtcEnabled,
2276
+ sendStartWebRtc: function sendStartWebRtc(offerSdp, voice, language, silenceMs, vadThreshold, allowInterruption) {
2277
+ self.connection.send('StartRealtimeWebRtc', self.getProfileId(), self.getSessionId() || '', offerSdp, voice, language, silenceMs, vadThreshold, allowInterruption);
2278
+ },
2279
+ webRtcIceServers: this.realtimeWebRtcIceServers || undefined,
2280
+ voiceName: this.realtimeVoiceName || '',
2281
+ getVoiceName: function getVoiceName() {
2282
+ return self.realtimeVoiceName || '';
2283
+ },
2284
+ realtimeEnabled: true,
2285
+ // The conversation button doubles as the realtime button on this host; the module owns its
2286
+ // click, label and state from here on.
2287
+ selectors: {
2288
+ realtimeButton: config.conversationButtonElementSelector
2289
+ },
2290
+ onActivate: function onActivate() {
2291
+ self.isConversationMode = true;
2292
+ self._conversationAssistantMessage = null;
2293
+ self._conversationPartialMessage = null;
2294
+ self.removeNotification('conversation-ended');
2295
+ },
2296
+ onDeactivate: function onDeactivate() {
2297
+ self.isConversationMode = false;
2298
+ self.finishRealtimeConversationCleanup();
2299
+ },
2300
+ onUserTurnPending: function onUserTurnPending(turnId) {
2301
+ self.addRealtimePendingTurn(turnId);
2302
+ },
2303
+ onUserTurnDropped: function onUserTurnDropped(turnId) {
2304
+ self.dropRealtimePendingTurn(turnId);
2305
+ }
2306
+ });
2307
+ },
2308
+ // Shared teardown tail: clears any in-flight streaming state and shows the conversation-ended
2309
+ // notification.
2310
+ finishRealtimeConversationCleanup: function finishRealtimeConversationCleanup() {
2311
+ if (this._conversationAssistantMessage) {
2312
+ var m = this.messages[this._conversationAssistantMessage.index];
2313
+ if (m) {
2314
+ m.isStreaming = false;
2315
+ }
2316
+ this._conversationAssistantMessage = null;
2317
+ }
2318
+ for (var i = 0; i < this.messages.length; i++) {
2319
+ if (this.messages[i].isStreaming) {
2320
+ this.messages[i].isStreaming = false;
2321
+ }
2322
+ }
2323
+ this.receiveNotification({
2324
+ type: 'conversation-ended',
2325
+ content: 'Conversation ended.',
2326
+ icon: 'fa-solid fa-circle-check',
2327
+ dismissible: true,
2328
+ autoDismissMs: 5000
2329
+ });
2330
+ },
2331
+ // The utterance has been captured but not transcribed yet. Showing a placeholder now puts the
2332
+ // prompt above the reply it produces: transcription lags the spoken answer, so a bubble added only
2333
+ // when the text arrives lands underneath its own reply.
2334
+ addRealtimePendingTurn: function addRealtimePendingTurn(turnId) {
2335
+ if (!turnId) {
2336
+ return;
2337
+ }
2338
+ this._realtimePendingTurns = this._realtimePendingTurns || {};
2339
+ var index = this.messages.length;
2340
+ this.addMessage({
2341
+ role: 'user',
2342
+ content: '',
2343
+ isPartial: true
2344
+ });
2345
+ this._realtimePendingTurns[turnId] = this.messages[index];
2346
+ },
2347
+ fillRealtimePendingTurn: function fillRealtimePendingTurn(pending, text) {
2348
+ if (!pending) {
2349
+ return;
2350
+ }
2351
+ if (!text) {
2352
+ var emptyAt = this.messages.indexOf(pending);
2353
+ if (emptyAt >= 0) {
2354
+ this.messages.splice(emptyAt, 1);
2355
+ }
2356
+ return;
2357
+ }
2358
+ var escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2359
+ pending.content = text;
2360
+ pending.htmlContent = '<p>' + escaped + '</p>';
2361
+ pending.isPartial = false;
2362
+ },
2363
+ dropRealtimePendingTurn: function dropRealtimePendingTurn(turnId) {
2364
+ if (!turnId || !this._realtimePendingTurns) {
2365
+ return;
2366
+ }
2367
+ var dropped = this._realtimePendingTurns[turnId];
2368
+ if (!dropped) {
2369
+ return;
2370
+ }
2371
+ delete this._realtimePendingTurns[turnId];
2372
+ var at = this.messages.indexOf(dropped);
2373
+ if (at >= 0) {
2374
+ this.messages.splice(at, 1);
2375
+ }
2376
+ },
2009
2377
  startConversationMode: function startConversationMode() {
2010
- var _this16 = this;
2378
+ var _this17 = this;
2011
2379
  if (!this.conversationModeEnabled || this.isConversationMode || !this.connection) {
2012
2380
  return;
2013
2381
  }
@@ -2026,99 +2394,36 @@ window.coreAIChatManager = function () {
2026
2394
  autoGainControl: true
2027
2395
  }
2028
2396
  }).then(function (stream) {
2029
- var mimeType = MediaRecorder.isTypeSupported('audio/ogg;codecs=opus') ? 'audio/ogg;codecs=opus' : MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
2030
- _this16.mediaRecorder = new MediaRecorder(stream, {
2031
- mimeType: mimeType,
2032
- audioBitsPerSecond: 128000
2033
- });
2034
- _this16._conversationSubject = new signalR.Subject();
2035
- _this16._conversationStream = stream;
2036
-
2037
- // Create an AnalyserNode for volume-based interrupt detection.
2038
- // During TTS playback, detect when the user speaks above
2039
- // the threshold to stop TTS (interrupt). Audio chunks are
2040
- // always forwarded browser echo cancellation handles
2041
- // speaker echo so the STT stream has no gaps.
2042
- var AudioCtx = window.AudioContext || window.webkitAudioContext;
2043
- if (AudioCtx) {
2044
- _this16._conversationAudioCtx = new AudioCtx();
2045
- _this16._conversationAnalyser = _this16._conversationAudioCtx.createAnalyser();
2046
- _this16._conversationAnalyser.fftSize = 256;
2047
- var micSource = _this16._conversationAudioCtx.createMediaStreamSource(stream);
2048
- micSource.connect(_this16._conversationAnalyser);
2049
- }
2050
- var pendingChunk = Promise.resolve();
2051
- var analyser = _this16._conversationAnalyser;
2052
- var interruptVolumeThreshold = 30;
2053
- _this16.mediaRecorder.addEventListener('dataavailable', function (e) {
2054
- if (e.data && e.data.size > 0) {
2055
- // During TTS playback, check mic volume to detect
2056
- // user interruption (speaking above threshold).
2057
- if (_this16.isPlayingAudio && analyser) {
2058
- var freqData = new Uint8Array(analyser.frequencyBinCount);
2059
- analyser.getByteFrequencyData(freqData);
2060
- var sum = 0;
2061
- for (var k = 0; k < freqData.length; k++) {
2062
- sum += freqData[k];
2063
- }
2064
- var avg = sum / freqData.length;
2065
- if (avg >= interruptVolumeThreshold) {
2066
- // User is speaking — interrupt TTS playback.
2067
- _this16.stopAudio();
2068
- }
2069
- }
2070
-
2071
- // Always send audio to STT — browser echo cancellation
2072
- // handles speaker echo; continuous audio avoids gaps
2073
- // that increase recognition latency.
2074
- pendingChunk = pendingChunk.then(/*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() {
2075
- var data, uint8Array, binaryString, base64;
2076
- return _regenerator().w(function (_context8) {
2077
- while (1) switch (_context8.n) {
2078
- case 0:
2079
- _context8.n = 1;
2080
- return e.data.arrayBuffer();
2081
- case 1:
2082
- data = _context8.v;
2083
- uint8Array = new Uint8Array(data);
2084
- binaryString = uint8Array.reduce(function (str, _byte2) {
2085
- return str + String.fromCharCode(_byte2);
2086
- }, '');
2087
- base64 = btoa(binaryString);
2088
- try {
2089
- _this16._conversationSubject.next(base64);
2090
- } catch (err) {
2091
- // Subject may have been completed already.
2092
- }
2093
- case 2:
2094
- return _context8.a(2);
2095
- }
2096
- }, _callee8);
2097
- })));
2397
+ _this17._conversationSubject = new signalR.Subject();
2398
+ _this17._conversationStream = stream;
2399
+
2400
+ // RMS (0..1) above which speech during TTS playback counts as an interrupt.
2401
+ var interruptRmsThreshold = 0.12;
2402
+
2403
+ // Capture raw 16 kHz PCM (16-bit mono) via Web Audio instead of MediaRecorder — see
2404
+ // startRecording for why WebM/Opus is avoided. The per-block RMS drives interrupt
2405
+ // detection during TTS playback, replacing the previous AnalyserNode.
2406
+ _this17._conversationCapture = _this17._createPcmCapture(stream, function (base64, rms) {
2407
+ if (_this17.isPlayingAudio && rms >= interruptRmsThreshold) {
2408
+ // User is speaking over TTS interrupt playback.
2409
+ _this17.stopAudio();
2410
+ }
2411
+ try {
2412
+ _this17._conversationSubject.next(base64);
2413
+ } catch (err) {
2414
+ // Subject may have been completed already.
2098
2415
  }
2099
2416
  });
2100
- _this16.mediaRecorder.addEventListener('stop', function () {
2101
- stream.getTracks().forEach(function (track) {
2102
- return track.stop();
2103
- });
2104
- pendingChunk.then(function () {
2105
- try {
2106
- _this16._conversationSubject.complete();
2107
- } catch (err) {
2108
- // Already completed.
2109
- }
2110
- });
2111
- });
2112
- var profileId = _this16.getProfileId();
2113
- var sessionId = _this16.getSessionId() || '';
2417
+ var profileId = _this17.getProfileId();
2418
+ var sessionId = _this17.getSessionId() || '';
2114
2419
  var language = navigator.language || document.documentElement.lang || 'en-US';
2115
- _this16.connection.send("StartConversation", profileId, sessionId, _this16._conversationSubject, mimeType, language);
2116
- _this16.mediaRecorder.start(250);
2117
- _this16.isRecording = true;
2420
+ var rate = _this17._conversationCapture && _this17._conversationCapture.sampleRate || 16000;
2421
+ _this17.connection.send("StartConversation", profileId, sessionId, _this17._conversationSubject, "audio/pcm;rate=" + rate, language);
2422
+ _this17.isRecording = true;
2118
2423
  })["catch"](function (err) {
2119
2424
  console.error('Microphone access denied:', err);
2120
- _this16.isConversationMode = false;
2121
- _this16.updateConversationButton();
2425
+ _this17.isConversationMode = false;
2426
+ _this17.updateConversationButton();
2122
2427
  });
2123
2428
  },
2124
2429
  stopConversationMode: function stopConversationMode() {
@@ -2132,21 +2437,20 @@ window.coreAIChatManager = function () {
2132
2437
  if (this.connection) {
2133
2438
  this.connection.invoke("StopConversation")["catch"](function () {});
2134
2439
  }
2135
- if (this.isRecording && this.mediaRecorder) {
2136
- this.mediaRecorder.stop();
2440
+ if (this.isRecording) {
2441
+ this._stopPcmCapture(this._conversationCapture);
2442
+ this._conversationCapture = null;
2443
+ if (this._conversationSubject) {
2444
+ try {
2445
+ this._conversationSubject.complete();
2446
+ } catch (err) {/* already completed */}
2447
+ }
2137
2448
  this.isRecording = false;
2138
2449
  }
2139
2450
  this.stopAudio();
2140
2451
  this._conversationPartialTranscript = '';
2141
2452
  this._conversationPartialMessage = null;
2142
2453
 
2143
- // Clean up the AudioContext used for volume monitoring.
2144
- if (this._conversationAudioCtx) {
2145
- this._conversationAudioCtx.close()["catch"](function () {});
2146
- this._conversationAudioCtx = null;
2147
- this._conversationAnalyser = null;
2148
- }
2149
-
2150
2454
  // Mark any in-flight assistant message as done to stop the spinner.
2151
2455
  if (this._conversationAssistantMessage) {
2152
2456
  var msg = this.messages[this._conversationAssistantMessage.index];
@@ -2245,7 +2549,7 @@ window.coreAIChatManager = function () {
2245
2549
  return removedCount;
2246
2550
  },
2247
2551
  receiveNotification: function receiveNotification(notification) {
2248
- var _this17 = this;
2552
+ var _this18 = this;
2249
2553
  if (!notification || !notification.type) {
2250
2554
  return;
2251
2555
  }
@@ -2260,7 +2564,7 @@ window.coreAIChatManager = function () {
2260
2564
  }
2261
2565
  this.scheduleNotificationDismiss(notification);
2262
2566
  this.$nextTick(function () {
2263
- _this17.scrollToBottom();
2567
+ _this18.scrollToBottom();
2264
2568
  });
2265
2569
  },
2266
2570
  updateNotification: function updateNotification(notification) {
@@ -2277,12 +2581,12 @@ window.coreAIChatManager = function () {
2277
2581
  }
2278
2582
  },
2279
2583
  scheduleNotificationDismiss: function scheduleNotificationDismiss(notification) {
2280
- var _this18 = this;
2584
+ var _this19 = this;
2281
2585
  if (!notification || !notification.type || !notification.autoDismissMs || notification.autoDismissMs <= 0) {
2282
2586
  return;
2283
2587
  }
2284
2588
  this.notificationDismissTimers[notification.type] = setTimeout(function () {
2285
- _this18.removeNotification(notification.type);
2589
+ _this19.removeNotification(notification.type);
2286
2590
  }, notification.autoDismissMs);
2287
2591
  },
2288
2592
  clearNotificationDismiss: function clearNotificationDismiss(notificationType) {
@@ -2312,12 +2616,12 @@ window.coreAIChatManager = function () {
2312
2616
  });
2313
2617
  },
2314
2618
  scrollToBottom: function scrollToBottom() {
2315
- var _this19 = this;
2619
+ var _this20 = this;
2316
2620
  if (!this.autoScroll) {
2317
2621
  return;
2318
2622
  }
2319
2623
  setTimeout(function () {
2320
- _this19.chatContainer.scrollTop = _this19.chatContainer.scrollHeight - _this19.chatContainer.clientHeight;
2624
+ _this20.chatContainer.scrollTop = _this20.chatContainer.scrollHeight - _this20.chatContainer.clientHeight;
2321
2625
  }, 50);
2322
2626
  },
2323
2627
  handleUserInput: function handleUserInput(event) {
@@ -2333,6 +2637,8 @@ window.coreAIChatManager = function () {
2333
2637
  this.inputElement.setAttribute('data-session-id', sessionId || '');
2334
2638
  },
2335
2639
  resetSession: function resetSession() {
2640
+ // Force-stop an in-progress voice/realtime conversation so it doesn't keep running under the new session.
2641
+ this.forceStopActiveVoice();
2336
2642
  this.stopRecording();
2337
2643
  this.rejectPendingSessionRequest('Session was reset.');
2338
2644
  this.setSessionId('');
@@ -2363,40 +2669,43 @@ window.coreAIChatManager = function () {
2363
2669
  if (!profileId || !this.connection) {
2364
2670
  return;
2365
2671
  }
2672
+
2673
+ // Force-stop any live voice/realtime session before starting a new chat.
2674
+ this.forceStopActiveVoice();
2366
2675
  this.requestNewSession(profileId)["catch"](function (err) {
2367
2676
  return console.error(err);
2368
2677
  });
2369
2678
  },
2370
2679
  ensureSessionForDocuments: function ensureSessionForDocuments(profileId) {
2371
- var _this20 = this;
2372
- return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
2680
+ var _this21 = this;
2681
+ return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() {
2373
2682
  var sessionId;
2374
- return _regenerator().w(function (_context9) {
2375
- while (1) switch (_context9.n) {
2683
+ return _regenerator().w(function (_context8) {
2684
+ while (1) switch (_context8.n) {
2376
2685
  case 0:
2377
- sessionId = _this20.getSessionId();
2686
+ sessionId = _this21.getSessionId();
2378
2687
  if (!sessionId) {
2379
- _context9.n = 1;
2688
+ _context8.n = 1;
2380
2689
  break;
2381
2690
  }
2382
- return _context9.a(2, sessionId);
2691
+ return _context8.a(2, sessionId);
2383
2692
  case 1:
2384
- if (!(!profileId || !_this20.connection)) {
2385
- _context9.n = 2;
2693
+ if (!(!profileId || !_this21.connection)) {
2694
+ _context8.n = 2;
2386
2695
  break;
2387
2696
  }
2388
- return _context9.a(2, null);
2697
+ return _context8.a(2, null);
2389
2698
  case 2:
2390
- _context9.n = 3;
2391
- return _this20.requestNewSession(profileId);
2699
+ _context8.n = 3;
2700
+ return _this21.requestNewSession(profileId);
2392
2701
  case 3:
2393
- return _context9.a(2, _context9.v);
2702
+ return _context8.a(2, _context8.v);
2394
2703
  }
2395
- }, _callee9);
2704
+ }, _callee8);
2396
2705
  }))();
2397
2706
  },
2398
2707
  requestNewSession: function requestNewSession(profileId) {
2399
- var _this21 = this;
2708
+ var _this22 = this;
2400
2709
  if (this.pendingSessionPromise) {
2401
2710
  return this.pendingSessionPromise;
2402
2711
  }
@@ -2404,14 +2713,14 @@ window.coreAIChatManager = function () {
2404
2713
  return Promise.resolve(null);
2405
2714
  }
2406
2715
  this.pendingSessionPromise = new Promise(function (resolve, reject) {
2407
- _this21.pendingSessionResolver = resolve;
2408
- _this21.pendingSessionRejector = reject;
2409
- _this21.pendingSessionTimeoutId = window.setTimeout(function () {
2410
- _this21.rejectPendingSessionRequest('Timed out while creating a chat session.');
2716
+ _this22.pendingSessionResolver = resolve;
2717
+ _this22.pendingSessionRejector = reject;
2718
+ _this22.pendingSessionTimeoutId = window.setTimeout(function () {
2719
+ _this22.rejectPendingSessionRequest('Timed out while creating a chat session.');
2411
2720
  }, 15000);
2412
2721
  });
2413
2722
  this.connection.invoke("StartSession", profileId, null)["catch"](function (err) {
2414
- _this21.rejectPendingSessionRequest(err);
2723
+ _this22.rejectPendingSessionRequest(err);
2415
2724
  });
2416
2725
  return this.pendingSessionPromise;
2417
2726
  },
@@ -2437,7 +2746,7 @@ window.coreAIChatManager = function () {
2437
2746
  this.pendingSessionTimeoutId = null;
2438
2747
  },
2439
2748
  initializeApp: function initializeApp() {
2440
- var _this22 = this;
2749
+ var _this23 = this;
2441
2750
  this.inputElement = document.querySelector(config.inputElementSelector);
2442
2751
  this.buttonElement = document.querySelector(config.sendButtonElementSelector);
2443
2752
  this.chatContainer = document.querySelector(config.chatContainerElementSelector);
@@ -2473,7 +2782,7 @@ window.coreAIChatManager = function () {
2473
2782
  fileInput.accept = config.allowedExtensions;
2474
2783
  }
2475
2784
  fileInput.addEventListener('change', function (e) {
2476
- return _this22.handleFileInputChange(e);
2785
+ return _this23.handleFileInputChange(e);
2477
2786
  });
2478
2787
  this.documentBar.parentElement.appendChild(fileInput);
2479
2788
 
@@ -2481,13 +2790,13 @@ window.coreAIChatManager = function () {
2481
2790
  var inputArea = this.inputElement ? this.inputElement.closest('.ai-admin-widget-input, .text-bg-light') : null;
2482
2791
  if (inputArea) {
2483
2792
  inputArea.addEventListener('dragover', function (e) {
2484
- return _this22.handleDragOver(e);
2793
+ return _this23.handleDragOver(e);
2485
2794
  });
2486
2795
  inputArea.addEventListener('dragleave', function (e) {
2487
- return _this22.handleDragLeave(e);
2796
+ return _this23.handleDragLeave(e);
2488
2797
  });
2489
2798
  inputArea.addEventListener('drop', function (e) {
2490
- return _this22.handleDrop(e);
2799
+ return _this23.handleDrop(e);
2491
2800
  });
2492
2801
  }
2493
2802
  }
@@ -2495,55 +2804,55 @@ window.coreAIChatManager = function () {
2495
2804
 
2496
2805
  // Pause auto-scroll when the user manually scrolls up during streaming.
2497
2806
  this.chatContainer.addEventListener('scroll', function () {
2498
- if (!_this22.stream) {
2807
+ if (!_this23.stream) {
2499
2808
  return;
2500
2809
  }
2501
2810
  var threshold = 30;
2502
- var atBottom = _this22.chatContainer.scrollHeight - _this22.chatContainer.clientHeight - _this22.chatContainer.scrollTop <= threshold;
2503
- _this22.autoScroll = atBottom;
2811
+ var atBottom = _this23.chatContainer.scrollHeight - _this23.chatContainer.clientHeight - _this23.chatContainer.scrollTop <= threshold;
2812
+ _this23.autoScroll = atBottom;
2504
2813
  });
2505
2814
  this.inputElement.addEventListener('keydown', function (event) {
2506
- if (_this22.stream != null) {
2815
+ if (_this23.stream != null) {
2507
2816
  return;
2508
2817
  }
2509
2818
  if (event.key === "Enter" && !event.shiftKey) {
2510
2819
  event.preventDefault();
2511
- _this22.buttonElement.click();
2820
+ _this23.buttonElement.click();
2512
2821
  }
2513
2822
  });
2514
2823
  this.inputElement.addEventListener('input', function (e) {
2515
- _this22.handleUserInput(e);
2824
+ _this23.handleUserInput(e);
2516
2825
  if (e.target.value.trim()) {
2517
- _this22.buttonElement.removeAttribute('disabled');
2826
+ _this23.buttonElement.removeAttribute('disabled');
2518
2827
  } else {
2519
- _this22.buttonElement.setAttribute('disabled', true);
2828
+ _this23.buttonElement.setAttribute('disabled', true);
2520
2829
  }
2521
2830
  });
2522
2831
  this.buttonElement.addEventListener('click', function () {
2523
- if (_this22.stream != null) {
2524
- _this22.stream.dispose();
2525
- _this22.stream = null;
2526
- _this22.streamingFinished();
2527
- _this22.hideTypingIndicator();
2832
+ if (_this23.stream != null) {
2833
+ _this23.stream.dispose();
2834
+ _this23.stream = null;
2835
+ _this23.streamingFinished();
2836
+ _this23.hideTypingIndicator();
2528
2837
 
2529
2838
  // Clean up: remove empty assistant message or stop streaming animation.
2530
- if (_this22.messages.length > 0) {
2531
- var lastMsg = _this22.messages[_this22.messages.length - 1];
2839
+ if (_this23.messages.length > 0) {
2840
+ var lastMsg = _this23.messages[_this23.messages.length - 1];
2532
2841
  if (lastMsg.role === 'assistant' && !lastMsg.content) {
2533
- _this22.messages.pop();
2842
+ _this23.messages.pop();
2534
2843
  } else if (lastMsg.isStreaming) {
2535
2844
  lastMsg.isStreaming = false;
2536
2845
  }
2537
2846
  }
2538
2847
  return;
2539
2848
  }
2540
- _this22.sendMessage();
2849
+ _this23.sendMessage();
2541
2850
  });
2542
2851
  var promptGenerators = document.getElementsByClassName('profile-generated-prompt');
2543
2852
  for (var i = 0; i < promptGenerators.length; i++) {
2544
2853
  promptGenerators[i].addEventListener('click', function (e) {
2545
2854
  e.preventDefault();
2546
- _this22.generatePrompt(e.target);
2855
+ _this23.generatePrompt(e.target);
2547
2856
  });
2548
2857
  }
2549
2858
  var chatSessions = document.getElementsByClassName('chat-session-history-item');
@@ -2555,8 +2864,8 @@ window.coreAIChatManager = function () {
2555
2864
  console.error('an element with the class chat-session-history-item with no data-session-id set.');
2556
2865
  return;
2557
2866
  }
2558
- _this22.loadSession(sessionId);
2559
- _this22.showChatScreen();
2867
+ _this23.loadSession(sessionId);
2868
+ _this23.showChatScreen();
2560
2869
  });
2561
2870
  }
2562
2871
  var initialMessages = Array.isArray(config.messages) ? config.messages : [];
@@ -2575,7 +2884,7 @@ window.coreAIChatManager = function () {
2575
2884
 
2576
2885
  // Update feedback icons in the DOM after initial messages have rendered.
2577
2886
  this.$nextTick(function () {
2578
- _this22.refreshAllFeedbackIcons();
2887
+ _this23.refreshAllFeedbackIcons();
2579
2888
  });
2580
2889
 
2581
2890
  // Delegate click for code block copy buttons.
@@ -2607,18 +2916,24 @@ window.coreAIChatManager = function () {
2607
2916
  if (this.micButton) {
2608
2917
  this.micButton.style.display = '';
2609
2918
  this.micButton.addEventListener('click', function () {
2610
- _this22.toggleRecording();
2919
+ _this23.toggleRecording();
2611
2920
  });
2612
2921
  }
2613
2922
  }
2614
2923
 
2615
- // Initialize conversation mode button.
2616
- if (this.conversationModeEnabled && config.conversationButtonElementSelector) {
2924
+ // Initialize conversation mode button (used for both STT/TTS conversation and realtime).
2925
+ if ((this.conversationModeEnabled || this.realtimeEnabled) && config.conversationButtonElementSelector) {
2617
2926
  this.conversationButton = document.querySelector(config.conversationButtonElementSelector);
2618
2927
  if (this.conversationButton) {
2619
- this.conversationButton.addEventListener('click', function () {
2620
- _this22.toggleConversationMode();
2621
- });
2928
+ if (this.realtimeEnabled) {
2929
+ // The shared realtime module owns the button (click, label, state) and adds the
2930
+ // voice settings popover next to it.
2931
+ this.setupRealtimeController(config);
2932
+ } else {
2933
+ this.conversationButton.addEventListener('click', function () {
2934
+ _this23.toggleConversationMode();
2935
+ });
2936
+ }
2622
2937
  }
2623
2938
  }
2624
2939
  return true;
@@ -2679,28 +2994,28 @@ window.coreAIChatManager = function () {
2679
2994
  this.copiedMessageIndex = -1;
2680
2995
  },
2681
2996
  copyResponse: function copyResponse(message, index, event) {
2682
- var _ref25,
2997
+ var _ref23,
2683
2998
  _message$copyContent,
2684
2999
  _event$target,
2685
3000
  _event$target$closest,
2686
- _this23 = this;
2687
- var text = message && _typeof(message) === 'object' ? (_ref25 = (_message$copyContent = message.copyContent) !== null && _message$copyContent !== void 0 ? _message$copyContent : message.content) !== null && _ref25 !== void 0 ? _ref25 : '' : message !== null && message !== void 0 ? message : '';
3001
+ _this24 = this;
3002
+ var text = message && _typeof(message) === 'object' ? (_ref23 = (_message$copyContent = message.copyContent) !== null && _message$copyContent !== void 0 ? _message$copyContent : message.content) !== null && _ref23 !== void 0 ? _ref23 : '' : message !== null && message !== void 0 ? message : '';
2688
3003
  var button = (event === null || event === void 0 ? void 0 : event.currentTarget) || (event === null || event === void 0 || (_event$target = event.target) === null || _event$target === void 0 || (_event$target$closest = _event$target.closest) === null || _event$target$closest === void 0 ? void 0 : _event$target$closest.call(_event$target, '[data-copy-message-index]')) || null;
2689
3004
  navigator.clipboard.writeText(text).then(function () {
2690
- _this23.clearCopiedMessageState();
2691
- _this23.copiedMessageIndex = typeof index === 'number' ? index : -1;
2692
- _this23.activeCopyButton = button;
3005
+ _this24.clearCopiedMessageState();
3006
+ _this24.copiedMessageIndex = typeof index === 'number' ? index : -1;
3007
+ _this24.activeCopyButton = button;
2693
3008
  if (button) {
2694
- _this23.setCopyButtonState(button, true);
3009
+ _this24.setCopyButtonState(button, true);
2695
3010
  } else {
2696
- _this23.$nextTick(function () {
2697
- return _this23.updateCopyButtons();
3011
+ _this24.$nextTick(function () {
3012
+ return _this24.updateCopyButtons();
2698
3013
  });
2699
3014
  }
2700
- _this23.copyResetTimeoutId = window.setTimeout(function () {
2701
- _this23.clearCopiedMessageState();
2702
- _this23.$nextTick(function () {
2703
- return _this23.updateCopyButtons();
3015
+ _this24.copyResetTimeoutId = window.setTimeout(function () {
3016
+ _this24.clearCopiedMessageState();
3017
+ _this24.$nextTick(function () {
3018
+ return _this24.updateCopyButtons();
2704
3019
  });
2705
3020
  }, Number(config.copyResetDelayMs) || 2000);
2706
3021
  })["catch"](function (err) {
@@ -2801,9 +3116,9 @@ window.coreAIChatManager = function () {
2801
3116
  // no longer mutes tracks; browser echo cancellation handles echo.
2802
3117
  },
2803
3118
  copiedMessageIndex: function copiedMessageIndex() {
2804
- var _this24 = this;
3119
+ var _this25 = this;
2805
3120
  this.$nextTick(function () {
2806
- return _this24.updateCopyButtons();
3121
+ return _this25.updateCopyButtons();
2807
3122
  });
2808
3123
  },
2809
3124
  isConversationMode: function isConversationMode(active) {
@@ -2827,28 +3142,28 @@ window.coreAIChatManager = function () {
2827
3142
  }
2828
3143
  },
2829
3144
  mounted: function mounted() {
2830
- var _this25 = this;
2831
- _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0() {
3145
+ var _this26 = this;
3146
+ _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
2832
3147
  var isInitialized;
2833
- return _regenerator().w(function (_context0) {
2834
- while (1) switch (_context0.n) {
3148
+ return _regenerator().w(function (_context9) {
3149
+ while (1) switch (_context9.n) {
2835
3150
  case 0:
2836
- _context0.n = 1;
2837
- return _this25.startConnection();
3151
+ _context9.n = 1;
3152
+ return _this26.startConnection();
2838
3153
  case 1:
2839
- isInitialized = _this25.initializeApp();
3154
+ isInitialized = _this26.initializeApp();
2840
3155
  if (isInitialized && hasWidgetConfig && widgetBehavior && typeof widgetBehavior.onMounted === 'function') {
2841
- widgetBehavior.onMounted(_this25, config);
3156
+ widgetBehavior.onMounted(_this26, config);
2842
3157
  }
2843
- _this25.$nextTick(function () {
2844
- _this25.updateCopyButtons();
2845
- refreshFontAwesomeIcons(_this25.$el);
2846
- _this25.fontAwesomeObserver = observeFontAwesomeIcons(_this25.$el);
3158
+ _this26.$nextTick(function () {
3159
+ _this26.updateCopyButtons();
3160
+ refreshFontAwesomeIcons(_this26.$el);
3161
+ _this26.fontAwesomeObserver = observeFontAwesomeIcons(_this26.$el);
2847
3162
  });
2848
3163
  case 2:
2849
- return _context0.a(2);
3164
+ return _context9.a(2);
2850
3165
  }
2851
- }, _callee0);
3166
+ }, _callee9);
2852
3167
  }))();
2853
3168
  window.addEventListener('beforeunload', this.handleBeforeUnload);
2854
3169
  window.addEventListener('crestapps-ai-chat-stop-tts', this.handleExternalTtsStop);
@@ -2936,6 +3251,7 @@ window.coreAIChatManager = function () {
2936
3251
  micButtonElementSelector: getAttributeValue(element, 'data-coreai-chat-mic-button-element-selector'),
2937
3252
  conversationButtonElementSelector: getAttributeValue(element, 'data-coreai-chat-conversation-button-element-selector'),
2938
3253
  ttsVoiceName: getAttributeValue(element, 'data-coreai-chat-tts-voice-name'),
3254
+ realtimeVoiceName: getAttributeValue(element, 'data-coreai-chat-realtime-voice-name'),
2939
3255
  documentBarSelector: getAttributeValue(element, 'data-coreai-chat-document-bar-selector'),
2940
3256
  uploadDocumentUrl: getAttributeValue(element, 'data-coreai-chat-upload-document-url'),
2941
3257
  removeDocumentUrl: getAttributeValue(element, 'data-coreai-chat-remove-document-url'),
@@ -2953,7 +3269,9 @@ window.coreAIChatManager = function () {
2953
3269
  metricsEnabled: 'data-coreai-chat-metrics-enabled',
2954
3270
  textToSpeechEnabled: 'data-coreai-chat-text-to-speech-enabled',
2955
3271
  sessionDocumentsEnabled: 'data-coreai-chat-session-documents-enabled',
2956
- singleResponseMode: 'data-coreai-chat-single-response-mode'
3272
+ singleResponseMode: 'data-coreai-chat-single-response-mode',
3273
+ realtimeEnabled: 'data-coreai-chat-realtime-enabled',
3274
+ realtimeWebRtcEnabled: 'data-coreai-chat-realtime-webrtc-enabled'
2957
3275
  };
2958
3276
  Object.keys(booleanAttributes).forEach(function (key) {
2959
3277
  var parsed = parseBooleanAttributeValue(getAttributeValue(element, booleanAttributes[key]));