@crestapps/ai-chat-ui 2.0.0-preview.193 → 2.0.0-preview.199

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.
@@ -432,7 +432,11 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
432
432
  // at the first one, so the sentence is torn into fragments and the commas between the markers are
433
433
  // left stranded on their own lines. Inline-block keeps the figure looking exactly the same while
434
434
  // remaining something a paragraph can legally contain.
435
- return "<span class=\"generated-image-container\">\n <img src=\"".concat(src, "\" alt=\"").concat(alt, "\" class=\"img-thumbnail\" style=\"max-width: ").concat(maxWidth, "px; height: auto;\" />\n <span class=\"mt-2 d-block\">\n <a href=\"").concat(src, "\" target=\"_blank\" download=\"").concat(alt, "\" title=\"").concat(defaultConfig.downloadImageTitle, "\" class=\"btn btn-sm btn-outline-secondary ai-download-image\">\n <i class=\"fa-solid fa-download\"></i>\n </a>\n </span>\n </span>");
435
+ // The download sits over the picture's lower corner rather than on a line of its own beneath it.
436
+ // A row of its own cost a band of vertical space under every figure, which a conversation full of
437
+ // them pays for once per figure. The link itself is unchanged -- same address, same download
438
+ // attribute, same behaviour -- only where it is drawn.
439
+ return "<span class=\"generated-image-container\">\n <img src=\"".concat(src, "\" alt=\"").concat(alt, "\" class=\"img-thumbnail\" style=\"max-width: ").concat(maxWidth, "px; height: auto;\" />\n <a href=\"").concat(src, "\" target=\"_blank\" download=\"").concat(alt, "\" title=\"").concat(defaultConfig.downloadImageTitle, "\" class=\"btn btn-sm btn-outline-secondary ai-download-image\">\n <i class=\"fa-solid fa-download\"></i>\n </a>\n </span>");
436
440
  };
437
441
 
438
442
  // Chart counter for unique IDs
@@ -505,14 +509,50 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
505
509
  // Deferred for the same reason the charts below are: the render that produced these elements has
506
510
  // not necessarily been flushed to the DOM when this runs.
507
511
  requestAnimationFrame(wireBrokenImageHandlers);
512
+ requestAnimationFrame(wireImageZoom);
508
513
  }
509
- function wireBrokenImageHandlers() {
510
- var images = document.querySelectorAll('.generated-image-container img:not([data-broken-wired])');
514
+
515
+ // A picture arrives after the markdown is handed back -- the view writes it, and a streamed answer
516
+ // rewrites it on every chunk -- so zooming cannot be attached once at load. It is attached on the same
517
+ // pass as the broken-image handler above, and the attribute marks a picture already attached so a
518
+ // re-render does not bind it a second time.
519
+ function wireImageZoom() {
520
+ // The library is the host's to load. Without it the picture is still shown and still downloadable;
521
+ // only the enlarging is missing, so this is not worth failing a render over.
522
+ if (typeof mediumZoom !== 'function') {
523
+ return;
524
+ }
525
+ var images = document.querySelectorAll('.generated-image-container img:not([data-zoom-wired])');
526
+ if (images.length === 0) {
527
+ return;
528
+ }
511
529
  var _iterator6 = _createForOfIteratorHelper(images),
512
530
  _step6;
513
531
  try {
514
- var _loop = function _loop() {
532
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
515
533
  var image = _step6.value;
534
+ image.setAttribute('data-zoom-wired', 'true');
535
+ }
536
+ } catch (err) {
537
+ _iterator6.e(err);
538
+ } finally {
539
+ _iterator6.f();
540
+ }
541
+ mediumZoom(images, {
542
+ // The drawing is bounded by the window rather than by this margin, so the margin is kept
543
+ // small: an ingested figure is read detail by detail, and every pixel given away here comes
544
+ // off the size it is read at.
545
+ margin: 16,
546
+ background: 'rgba(15, 15, 15, 0.92)'
547
+ });
548
+ }
549
+ function wireBrokenImageHandlers() {
550
+ var images = document.querySelectorAll('.generated-image-container img:not([data-broken-wired])');
551
+ var _iterator7 = _createForOfIteratorHelper(images),
552
+ _step7;
553
+ try {
554
+ var _loop = function _loop() {
555
+ var image = _step7.value;
516
556
  image.setAttribute('data-broken-wired', 'true');
517
557
  image.addEventListener('error', function () {
518
558
  var container = image.closest('.generated-image-container');
@@ -529,13 +569,13 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
529
569
  once: true
530
570
  });
531
571
  };
532
- for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
572
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
533
573
  _loop();
534
574
  }
535
575
  } catch (err) {
536
- _iterator6.e(err);
576
+ _iterator7.e(err);
537
577
  } finally {
538
- _iterator6.f();
578
+ _iterator7.f();
539
579
  }
540
580
  }
541
581
  function renderMessageMedia(message) {
@@ -551,11 +591,11 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
551
591
  // Defer to requestAnimationFrame so the browser has fully laid out the
552
592
  // canvas elements before Chart.js reads their dimensions.
553
593
  requestAnimationFrame(function () {
554
- var _iterator7 = _createForOfIteratorHelper(charts),
555
- _step7;
594
+ var _iterator8 = _createForOfIteratorHelper(charts),
595
+ _step8;
556
596
  try {
557
- for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
558
- var c = _step7.value;
597
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
598
+ var c = _step8.value;
559
599
  var canvas = document.getElementById(c.chartId);
560
600
  if (!canvas) {
561
601
  continue;
@@ -588,9 +628,9 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
588
628
  }
589
629
  }
590
630
  } catch (err) {
591
- _iterator7.e(err);
631
+ _iterator8.e(err);
592
632
  } finally {
593
- _iterator7.f();
633
+ _iterator8.f();
594
634
  }
595
635
  });
596
636
  }
@@ -691,6 +731,11 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
691
731
  isConversationMode: false,
692
732
  // Realtime (speech-to-speech) controller from the shared CoreAIRealtime module.
693
733
  realtimeController: null,
734
+ realtimeButton: null,
735
+ // Whether a realtime deployment actually resolved. The controller is attached whenever the
736
+ // module is loaded, so its presence says nothing about which transport carries the
737
+ // conversation -- only this flag does.
738
+ realtimeEnabled: config.realtimeEnabled === true,
694
739
  notifications: [],
695
740
  notificationDismissTimers: {},
696
741
  copyTitle: config.copyTitle,
@@ -1095,6 +1140,11 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
1095
1140
  }
1096
1141
  return _context2.a(2);
1097
1142
  case 1:
1143
+ // A live voice session and a typed turn are two writers into one interaction, and they
1144
+ // would interleave. End the session first; the turns it already produced are persisted, so
1145
+ // the typed message simply continues the same thread.
1146
+ _this4.endActiveVoiceSession();
1147
+
1098
1148
  // Stop any active recording before sending.
1099
1149
  if (_this4.isRecording) {
1100
1150
  _this4.stopRecording();
@@ -1395,18 +1445,18 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
1395
1445
  }, 0);
1396
1446
  var combined = new Uint8Array(totalLength);
1397
1447
  var offset = 0;
1398
- var _iterator8 = _createForOfIteratorHelper(this.audioChunks),
1399
- _step8;
1448
+ var _iterator9 = _createForOfIteratorHelper(this.audioChunks),
1449
+ _step9;
1400
1450
  try {
1401
- for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
1402
- var chunk = _step8.value;
1451
+ for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
1452
+ var chunk = _step9.value;
1403
1453
  combined.set(chunk, offset);
1404
1454
  offset += chunk.length;
1405
1455
  }
1406
1456
  } catch (err) {
1407
- _iterator8.e(err);
1457
+ _iterator9.e(err);
1408
1458
  } finally {
1409
- _iterator8.f();
1459
+ _iterator9.f();
1410
1460
  }
1411
1461
  this.audioChunks = [];
1412
1462
  var blob = new Blob([combined], {
@@ -1489,6 +1539,39 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
1489
1539
  return _this12.updateTtsPlaybackButtons();
1490
1540
  });
1491
1541
  },
1542
+ // A live voice session takes the message box away and puts the audio settings in its place; ending
1543
+ // the session gives it back. Sending a typed message already ends the session first, so the box was
1544
+ // never a way to say something *during* one -- hiding it only makes that honest, and frees the row
1545
+ // for the gear. Both transports land here: realtime through the module's onActivate, the
1546
+ // speech-to-text cascade through startConversationMode.
1547
+ setVoiceSessionLive: function setVoiceSessionLive(live) {
1548
+ if (this.inputElement) {
1549
+ this.inputElement.hidden = live;
1550
+ }
1551
+ if (this.buttonElement) {
1552
+ this.buttonElement.hidden = live;
1553
+ }
1554
+
1555
+ // With the message box gone the live button would otherwise sit at its natural width in an
1556
+ // empty row, so it takes the space the box left behind. Whichever transport is carrying the
1557
+ // conversation owns a different button, and the hidden one is unaffected by the class.
1558
+ [this.realtimeButton, this.conversationButton].forEach(function (button) {
1559
+ if (button) {
1560
+ button.classList.toggle('flex-grow-1', live);
1561
+ }
1562
+ });
1563
+
1564
+ // The gear is built by the realtime module directly after the button it was handed, and only
1565
+ // in realtime mode, so it is absent on the cascade. It belongs to a live session rather than
1566
+ // to the surface. The module hard-codes its element id and a page can host two chat clients
1567
+ // at once -- this one and the admin widget -- so it is found next to this client's own button
1568
+ // rather than by id.
1569
+ var next = this.realtimeButton && this.realtimeButton.nextElementSibling;
1570
+ var audioSettings = next && next.id === 'realtime-audio-settings' ? next : null;
1571
+ if (audioSettings) {
1572
+ audioSettings.hidden = !live;
1573
+ }
1574
+ },
1492
1575
  toggleConversationMode: function toggleConversationMode() {
1493
1576
  if (this.isConversationMode) {
1494
1577
  this.stopConversationMode();
@@ -1496,6 +1579,17 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
1496
1579
  this.startConversationMode();
1497
1580
  }
1498
1581
  },
1582
+ // Two writers into one interaction would interleave, so the voice session ends before the typed
1583
+ // turn starts rather than running alongside it.
1584
+ endActiveVoiceSession: function endActiveVoiceSession() {
1585
+ if (this.realtimeController && this.realtimeController.isActive()) {
1586
+ this.realtimeController.stop();
1587
+ return;
1588
+ }
1589
+ if (this.isConversationMode) {
1590
+ this.stopConversationMode();
1591
+ }
1592
+ },
1499
1593
  startConversationMode: function startConversationMode() {
1500
1594
  var _this13 = this;
1501
1595
  if (!this.conversationModeEnabled || this.isConversationMode || !this.connection) {
@@ -2130,6 +2224,8 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
2130
2224
  // module owns the mic capture, playback, echo guard, push-to-talk and audio settings; the
2131
2225
  // callbacks below plug it into this app's connection and conversation-transcript display.
2132
2226
  if (window.CoreAIRealtime && config.realtimeButtonElementSelector) {
2227
+ // Kept so the voice settings popover can be found next to this client's own button.
2228
+ this.realtimeButton = document.querySelector(config.realtimeButtonElementSelector);
2133
2229
  this.realtimeController = window.CoreAIRealtime.attach({
2134
2230
  connection: this.connection,
2135
2231
  ensureConnected: function ensureConnected() {
@@ -2152,13 +2248,23 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
2152
2248
  },
2153
2249
  capableDeployments: config.realtimeCapableDeployments || [],
2154
2250
  realtimeEnabled: config.realtimeEnabled === true,
2251
+ // input and sendButton are deliberately not handed over. The module hides every
2252
+ // control it is given when realtime takes over, and typing has to stay available
2253
+ // during a voice conversation -- that is the whole feature.
2254
+ //
2255
+ // deploymentSelect is not handed over either: it would read the raw select value,
2256
+ // and an empty conversation deployment means "use the site default", which still
2257
+ // resolves. The host resolves it and calls applyMode itself (see below).
2155
2258
  selectors: {
2156
2259
  realtimeButton: config.realtimeButtonElementSelector,
2157
- input: config.inputElementSelector,
2158
- sendButton: config.sendButtonElementSelector,
2159
2260
  micButton: config.micButtonElementSelector,
2160
- conversationButton: config.conversationButtonElementSelector,
2161
- deploymentSelect: config.deploymentSelectElementSelector
2261
+ conversationButton: config.conversationButtonElementSelector
2262
+ },
2263
+ // The module owns whether a session is live, so the message box follows its flag
2264
+ // rather than this app's copy of it. Losing one of these events used to leave the
2265
+ // box hidden under a button that had already gone back to "Start speaking".
2266
+ onSessionStateChanged: function onSessionStateChanged(active) {
2267
+ _this18.setVoiceSessionLive(active);
2162
2268
  },
2163
2269
  onActivate: function onActivate() {
2164
2270
  _this18.isConversationMode = true;
@@ -2219,6 +2325,11 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
2219
2325
  }
2220
2326
  });
2221
2327
  this.setupRealtimeVoicePicker(config);
2328
+
2329
+ // The module reveals the gear as soon as the surface enters realtime mode, but it belongs
2330
+ // to a live session. The watcher that would hide it only runs on a change, so the idle
2331
+ // state has to be stated once here.
2332
+ this.setVoiceSessionLive(false);
2222
2333
  }
2223
2334
  },
2224
2335
  // Populates the per-interaction realtime voice picker from the selected realtime deployment's
@@ -2232,28 +2343,46 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
2232
2343
  return;
2233
2344
  }
2234
2345
  var voiceGroup = config.realtimeVoiceGroupElementSelector ? document.querySelector(config.realtimeVoiceGroupElementSelector) : null;
2235
- var deploymentSelect = config.deploymentSelectElementSelector ? document.querySelector(config.deploymentSelectElementSelector) : null;
2346
+ var conversationSelect = config.conversationDeploymentSelectElementSelector ? document.querySelector(config.conversationDeploymentSelectElementSelector) : null;
2236
2347
  var capable = (config.realtimeCapableDeployments || []).map(function (n) {
2237
2348
  return (n || '').toLowerCase();
2238
2349
  });
2239
2350
  var savedVoiceId = config.realtimeVoiceName || '';
2351
+ var self = this;
2240
2352
  function isRealtimeDeployment(name) {
2241
2353
  return name && capable.indexOf(name.toLowerCase()) !== -1;
2242
2354
  }
2355
+
2356
+ // Mirrors the realtime slot's chain on the server: the interaction's own choice, then the
2357
+ // site default, then the first realtime-capable deployment. Only conversation mode reaches
2358
+ // it -- an interaction that is not in conversation mode never speaks.
2359
+ function resolveConversationDeployment() {
2360
+ if (!self.conversationModeEnabled) {
2361
+ return '';
2362
+ }
2363
+ var name = conversationSelect && conversationSelect.value || config.defaultRealtimeDeploymentName || (capable.length ? capable[0] : '');
2364
+ return isRealtimeDeployment(name) ? name : '';
2365
+ }
2243
2366
  function loadVoices() {
2244
2367
  return _loadVoices.apply(this, arguments);
2245
2368
  }
2246
2369
  function _loadVoices() {
2247
2370
  _loadVoices = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() {
2248
- var deploymentName, realtime, response, contentType, result, grouped, _iterator9, _step9, voice, groupName, _t2;
2371
+ var deploymentName, realtime, response, contentType, result, grouped, _iterator0, _step0, voice, groupName, _t2;
2249
2372
  return _regenerator().w(function (_context4) {
2250
2373
  while (1) switch (_context4.p = _context4.n) {
2251
2374
  case 0:
2252
- deploymentName = deploymentSelect ? deploymentSelect.value : '';
2253
- realtime = isRealtimeDeployment(deploymentName);
2375
+ deploymentName = resolveConversationDeployment();
2376
+ realtime = !!deploymentName;
2254
2377
  if (voiceGroup) {
2255
2378
  voiceGroup.classList.toggle('d-none', !realtime);
2256
2379
  }
2380
+
2381
+ // Changing the conversation deployment switches the voice toggle on or off without a
2382
+ // reload.
2383
+ if (self.realtimeController) {
2384
+ self.realtimeController.applyMode(realtime);
2385
+ }
2257
2386
  if (realtime) {
2258
2387
  _context4.n = 1;
2259
2388
  break;
@@ -2287,10 +2416,10 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
2287
2416
  return _context4.a(2);
2288
2417
  case 6:
2289
2418
  grouped = new Map();
2290
- _iterator9 = _createForOfIteratorHelper(result.voices);
2419
+ _iterator0 = _createForOfIteratorHelper(result.voices);
2291
2420
  try {
2292
- for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
2293
- voice = _step9.value;
2421
+ for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
2422
+ voice = _step0.value;
2294
2423
  groupName = voice.gender || 'Voices';
2295
2424
  if (!grouped.has(groupName)) {
2296
2425
  grouped.set(groupName, []);
@@ -2298,9 +2427,9 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
2298
2427
  grouped.get(groupName).push(voice);
2299
2428
  }
2300
2429
  } catch (err) {
2301
- _iterator9.e(err);
2430
+ _iterator0.e(err);
2302
2431
  } finally {
2303
- _iterator9.f();
2432
+ _iterator0.f();
2304
2433
  }
2305
2434
  Array.from(grouped.keys()).sort(function (a, b) {
2306
2435
  return a.localeCompare(b);
@@ -2328,8 +2457,8 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
2328
2457
  }));
2329
2458
  return _loadVoices.apply(this, arguments);
2330
2459
  }
2331
- if (deploymentSelect) {
2332
- deploymentSelect.addEventListener('change', loadVoices);
2460
+ if (conversationSelect) {
2461
+ conversationSelect.addEventListener('change', loadVoices);
2333
2462
  }
2334
2463
  loadVoices();
2335
2464
  },
@@ -2621,17 +2750,20 @@ window.chatInteractionManager = function (_window$CoreAIChatMar, _window$CoreAIC
2621
2750
  // no longer mutes tracks; browser echo cancellation handles echo.
2622
2751
  },
2623
2752
  isConversationMode: function isConversationMode(active) {
2753
+ // The dictation microphone is hidden while a voice conversation runs: the conversation
2754
+ // already owns the microphone, and a second one would mean something different.
2624
2755
  if (this.micButton) {
2625
2756
  this.micButton.style.display = active ? 'none' : this.speechToTextEnabled ? '' : 'none';
2626
2757
  }
2627
- if (this.buttonElement) {
2628
- this.buttonElement.style.display = active ? 'none' : '';
2629
- }
2630
- if (this.inputElement) {
2631
- this.inputElement.disabled = active;
2632
- if (active) {
2633
- this.inputElement.placeholder = '';
2634
- }
2758
+
2759
+ // The message box and the send button give way to the voice settings while the conversation
2760
+ // runs, and come back when it ends. Both kinds of turn still land in the same thread; what
2761
+ // they cannot do is overlap.
2762
+ //
2763
+ // Only the cascade is driven from here. Realtime follows the module's own session flag
2764
+ // (onSessionStateChanged), so the two never disagree about whether a session is live.
2765
+ if (!this.realtimeEnabled) {
2766
+ this.setVoiceSessionLive(active);
2635
2767
  }
2636
2768
  },
2637
2769
  copiedMessageIndex: function copiedMessageIndex() {