@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-widget.js +4 -1
- package/dist/ai-chat-widget.js.map +1 -1
- package/dist/ai-chat-widget.min.js +1 -1
- package/dist/ai-chat-widget.min.js.map +1 -1
- package/dist/ai-chat.js +638 -320
- package/dist/ai-chat.js.map +1 -1
- package/dist/ai-chat.min.js +1 -1
- package/dist/ai-chat.min.js.map +1 -1
- package/dist/chat-interaction.js +323 -77
- package/dist/chat-interaction.js.map +1 -1
- package/dist/chat-interaction.min.js +1 -1
- package/dist/chat-interaction.min.js.map +1 -1
- package/dist/realtime-audio.js +2236 -0
- package/dist/realtime-audio.js.map +1 -0
- package/dist/realtime-audio.min.js +2 -0
- package/dist/realtime-audio.min.js.map +1 -0
- package/package.json +5 -1
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
@@ -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
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1573
|
+
_this6.scrollToBottom();
|
|
1446
1574
|
});
|
|
1447
1575
|
},
|
|
1448
1576
|
addMessages: function addMessages(messages) {
|
|
1449
|
-
var
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1544
|
-
|
|
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 =
|
|
1552
|
-
var sessionId =
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
|
1697
|
+
if (!this.isRecording) {
|
|
1597
1698
|
return;
|
|
1598
1699
|
}
|
|
1599
|
-
this.
|
|
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
|
|
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 =
|
|
1849
|
+
var message = _this9.messages[messageIndex];
|
|
1641
1850
|
if (!message) {
|
|
1642
1851
|
if (chunk.sessionId && !currentSessionId) {
|
|
1643
|
-
|
|
1852
|
+
_this9.initializeSession(chunk.sessionId);
|
|
1644
1853
|
}
|
|
1645
|
-
|
|
1854
|
+
_this9.hideTypingIndicator();
|
|
1646
1855
|
// Re-assign the index after hiding the typing indicator.
|
|
1647
|
-
messageIndex =
|
|
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
|
-
|
|
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
|
-
|
|
1688
|
-
|
|
1896
|
+
_this9.messages[messageIndex] = message;
|
|
1897
|
+
_this9.$nextTick(function () {
|
|
1689
1898
|
renderChartsInMessage(message);
|
|
1690
|
-
|
|
1899
|
+
_this9.scrollToBottom();
|
|
1691
1900
|
});
|
|
1692
1901
|
},
|
|
1693
1902
|
complete: function complete() {
|
|
1694
|
-
var
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
var msg =
|
|
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
|
-
|
|
1912
|
+
_this9.hideTypingIndicator();
|
|
1704
1913
|
}
|
|
1705
1914
|
|
|
1706
1915
|
// Trigger text-to-speech only in conversation mode.
|
|
1707
|
-
if (
|
|
1708
|
-
|
|
1916
|
+
if (_this9.isConversationMode && _this9.textToSpeechEnabled && msg && msg.content) {
|
|
1917
|
+
_this9.synthesizeSpeech(msg.content);
|
|
1709
1918
|
}
|
|
1710
|
-
(
|
|
1711
|
-
|
|
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
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
var msg =
|
|
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
|
-
|
|
1722
|
-
if (!
|
|
1723
|
-
|
|
1930
|
+
_this9.hideTypingIndicator();
|
|
1931
|
+
if (!_this9.isNavigatingAway) {
|
|
1932
|
+
_this9.addMessage(_this9.getServiceDownMessage());
|
|
1724
1933
|
}
|
|
1725
|
-
(
|
|
1726
|
-
|
|
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
|
|
1957
|
+
var _this0 = this;
|
|
1749
1958
|
references = normalizeReferences(references);
|
|
1750
1959
|
if (Object.keys(references).length) {
|
|
1751
|
-
var
|
|
1960
|
+
var _ref22, _message$rawContent2;
|
|
1752
1961
|
var message = this.messages[messageIndex];
|
|
1753
|
-
message.rawContent = (
|
|
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
|
-
|
|
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
|
|
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 ===
|
|
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
|
|
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 ===
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1946
|
-
|
|
2154
|
+
_this14.currentAudioElement = null;
|
|
2155
|
+
_this14.currentAudioUrl = null;
|
|
1947
2156
|
URL.revokeObjectURL(url);
|
|
1948
|
-
|
|
2157
|
+
_this14.playNextInQueue();
|
|
1949
2158
|
});
|
|
1950
2159
|
audio.addEventListener('error', function () {
|
|
1951
|
-
|
|
1952
|
-
|
|
2160
|
+
_this14.currentAudioElement = null;
|
|
2161
|
+
_this14.currentAudioUrl = null;
|
|
1953
2162
|
URL.revokeObjectURL(url);
|
|
1954
|
-
|
|
2163
|
+
_this14.playNextInQueue();
|
|
1955
2164
|
});
|
|
1956
2165
|
audio.play()["catch"](function (err) {
|
|
1957
2166
|
console.error("Audio playback error:", err);
|
|
1958
|
-
|
|
1959
|
-
|
|
2167
|
+
_this14.currentAudioElement = null;
|
|
2168
|
+
_this14.currentAudioUrl = null;
|
|
1960
2169
|
URL.revokeObjectURL(url);
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
return
|
|
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
|
|
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
|
|
2187
|
+
return _this15.updateTtsPlaybackButtons();
|
|
1979
2188
|
});
|
|
1980
2189
|
this.conversationModeOnAudioEnded();
|
|
1981
2190
|
}
|
|
1982
2191
|
},
|
|
1983
2192
|
stopAudio: function stopAudio() {
|
|
1984
|
-
var
|
|
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
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
|
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
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
//
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
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
|
-
|
|
2101
|
-
|
|
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
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
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
|
-
|
|
2121
|
-
|
|
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
|
|
2136
|
-
this.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2619
|
+
var _this20 = this;
|
|
2316
2620
|
if (!this.autoScroll) {
|
|
2317
2621
|
return;
|
|
2318
2622
|
}
|
|
2319
2623
|
setTimeout(function () {
|
|
2320
|
-
|
|
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
|
|
2372
|
-
return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function
|
|
2680
|
+
var _this21 = this;
|
|
2681
|
+
return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() {
|
|
2373
2682
|
var sessionId;
|
|
2374
|
-
return _regenerator().w(function (
|
|
2375
|
-
while (1) switch (
|
|
2683
|
+
return _regenerator().w(function (_context8) {
|
|
2684
|
+
while (1) switch (_context8.n) {
|
|
2376
2685
|
case 0:
|
|
2377
|
-
sessionId =
|
|
2686
|
+
sessionId = _this21.getSessionId();
|
|
2378
2687
|
if (!sessionId) {
|
|
2379
|
-
|
|
2688
|
+
_context8.n = 1;
|
|
2380
2689
|
break;
|
|
2381
2690
|
}
|
|
2382
|
-
return
|
|
2691
|
+
return _context8.a(2, sessionId);
|
|
2383
2692
|
case 1:
|
|
2384
|
-
if (!(!profileId || !
|
|
2385
|
-
|
|
2693
|
+
if (!(!profileId || !_this21.connection)) {
|
|
2694
|
+
_context8.n = 2;
|
|
2386
2695
|
break;
|
|
2387
2696
|
}
|
|
2388
|
-
return
|
|
2697
|
+
return _context8.a(2, null);
|
|
2389
2698
|
case 2:
|
|
2390
|
-
|
|
2391
|
-
return
|
|
2699
|
+
_context8.n = 3;
|
|
2700
|
+
return _this21.requestNewSession(profileId);
|
|
2392
2701
|
case 3:
|
|
2393
|
-
return
|
|
2702
|
+
return _context8.a(2, _context8.v);
|
|
2394
2703
|
}
|
|
2395
|
-
},
|
|
2704
|
+
}, _callee8);
|
|
2396
2705
|
}))();
|
|
2397
2706
|
},
|
|
2398
2707
|
requestNewSession: function requestNewSession(profileId) {
|
|
2399
|
-
var
|
|
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
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2793
|
+
return _this23.handleDragOver(e);
|
|
2485
2794
|
});
|
|
2486
2795
|
inputArea.addEventListener('dragleave', function (e) {
|
|
2487
|
-
return
|
|
2796
|
+
return _this23.handleDragLeave(e);
|
|
2488
2797
|
});
|
|
2489
2798
|
inputArea.addEventListener('drop', function (e) {
|
|
2490
|
-
return
|
|
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 (!
|
|
2807
|
+
if (!_this23.stream) {
|
|
2499
2808
|
return;
|
|
2500
2809
|
}
|
|
2501
2810
|
var threshold = 30;
|
|
2502
|
-
var atBottom =
|
|
2503
|
-
|
|
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 (
|
|
2815
|
+
if (_this23.stream != null) {
|
|
2507
2816
|
return;
|
|
2508
2817
|
}
|
|
2509
2818
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
2510
2819
|
event.preventDefault();
|
|
2511
|
-
|
|
2820
|
+
_this23.buttonElement.click();
|
|
2512
2821
|
}
|
|
2513
2822
|
});
|
|
2514
2823
|
this.inputElement.addEventListener('input', function (e) {
|
|
2515
|
-
|
|
2824
|
+
_this23.handleUserInput(e);
|
|
2516
2825
|
if (e.target.value.trim()) {
|
|
2517
|
-
|
|
2826
|
+
_this23.buttonElement.removeAttribute('disabled');
|
|
2518
2827
|
} else {
|
|
2519
|
-
|
|
2828
|
+
_this23.buttonElement.setAttribute('disabled', true);
|
|
2520
2829
|
}
|
|
2521
2830
|
});
|
|
2522
2831
|
this.buttonElement.addEventListener('click', function () {
|
|
2523
|
-
if (
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
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 (
|
|
2531
|
-
var lastMsg =
|
|
2839
|
+
if (_this23.messages.length > 0) {
|
|
2840
|
+
var lastMsg = _this23.messages[_this23.messages.length - 1];
|
|
2532
2841
|
if (lastMsg.role === 'assistant' && !lastMsg.content) {
|
|
2533
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2559
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2620
|
-
|
|
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
|
|
2997
|
+
var _ref23,
|
|
2683
2998
|
_message$copyContent,
|
|
2684
2999
|
_event$target,
|
|
2685
3000
|
_event$target$closest,
|
|
2686
|
-
|
|
2687
|
-
var text = message && _typeof(message) === 'object' ? (
|
|
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
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
3005
|
+
_this24.clearCopiedMessageState();
|
|
3006
|
+
_this24.copiedMessageIndex = typeof index === 'number' ? index : -1;
|
|
3007
|
+
_this24.activeCopyButton = button;
|
|
2693
3008
|
if (button) {
|
|
2694
|
-
|
|
3009
|
+
_this24.setCopyButtonState(button, true);
|
|
2695
3010
|
} else {
|
|
2696
|
-
|
|
2697
|
-
return
|
|
3011
|
+
_this24.$nextTick(function () {
|
|
3012
|
+
return _this24.updateCopyButtons();
|
|
2698
3013
|
});
|
|
2699
3014
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
return
|
|
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
|
|
3119
|
+
var _this25 = this;
|
|
2805
3120
|
this.$nextTick(function () {
|
|
2806
|
-
return
|
|
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
|
|
2831
|
-
_asyncToGenerator(/*#__PURE__*/_regenerator().m(function
|
|
3145
|
+
var _this26 = this;
|
|
3146
|
+
_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
|
|
2832
3147
|
var isInitialized;
|
|
2833
|
-
return _regenerator().w(function (
|
|
2834
|
-
while (1) switch (
|
|
3148
|
+
return _regenerator().w(function (_context9) {
|
|
3149
|
+
while (1) switch (_context9.n) {
|
|
2835
3150
|
case 0:
|
|
2836
|
-
|
|
2837
|
-
return
|
|
3151
|
+
_context9.n = 1;
|
|
3152
|
+
return _this26.startConnection();
|
|
2838
3153
|
case 1:
|
|
2839
|
-
isInitialized =
|
|
3154
|
+
isInitialized = _this26.initializeApp();
|
|
2840
3155
|
if (isInitialized && hasWidgetConfig && widgetBehavior && typeof widgetBehavior.onMounted === 'function') {
|
|
2841
|
-
widgetBehavior.onMounted(
|
|
3156
|
+
widgetBehavior.onMounted(_this26, config);
|
|
2842
3157
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
refreshFontAwesomeIcons(
|
|
2846
|
-
|
|
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
|
|
3164
|
+
return _context9.a(2);
|
|
2850
3165
|
}
|
|
2851
|
-
},
|
|
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]));
|