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

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
@@ -811,8 +811,10 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
811
811
  mediaRecorder: null,
812
812
  preRecordingPrompt: '',
813
813
  micButton: null,
814
- speechToTextEnabled: config.chatMode === 'AudioInput' || config.chatMode === 'Conversation',
815
- textToSpeechEnabled: config.chatMode === 'Conversation' || !!config.textToSpeechEnabled,
814
+ // Conversation mode is carried either by a realtime session or by the speech-to-text plus
815
+ // text-to-speech cascade, never both. Dictation and spoken playback belong to the cascade.
816
+ speechToTextEnabled: !config.realtimeEnabled && (config.chatMode === 'AudioInput' || config.chatMode === 'Conversation'),
817
+ textToSpeechEnabled: !config.realtimeEnabled && config.chatMode === 'Conversation' || !!config.textToSpeechEnabled,
816
818
  ttsVoiceName: config.ttsVoiceName || null,
817
819
  audioChunks: [],
818
820
  audioPlayQueue: [],
@@ -835,6 +837,7 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
835
837
  realtimeWebRtcIceServers: Array.isArray(config.realtimeWebRtcIceServers) && config.realtimeWebRtcIceServers.length ? config.realtimeWebRtcIceServers : null,
836
838
  realtimeVoiceName: config.realtimeVoiceName || null,
837
839
  conversationButton: null,
840
+ realtimeHintElement: config.realtimeHintElementSelector ? document.querySelector(config.realtimeHintElementSelector) : null,
838
841
  isConversationMode: false,
839
842
  notificationDismissTimers: {},
840
843
  pendingSessionPromise: null,
@@ -1827,6 +1830,11 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
1827
1830
  return;
1828
1831
  }
1829
1832
 
1833
+ // A live voice session and a typed turn are two writers into one chat session, and they
1834
+ // would interleave. End the session first; the turns it already produced are persisted, so
1835
+ // the typed message simply continues the same thread.
1836
+ this.forceStopActiveVoice();
1837
+
1830
1838
  // Stop any active recording before sending.
1831
1839
  if (this.isRecording) {
1832
1840
  this.stopRecording();
@@ -2399,6 +2407,45 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
2399
2407
  return _this16.updateTtsPlaybackButtons();
2400
2408
  });
2401
2409
  },
2410
+ // A live voice session takes the message box away and puts the audio settings in its place; ending
2411
+ // the session gives it back. Sending a typed message already force-stopped the session, so the box
2412
+ // was never a way to say something *during* one -- hiding it only makes that honest, and frees the
2413
+ // row for the gear. Both transports land here: realtime through the module's onActivate, the
2414
+ // speech-to-text cascade through startConversationMode.
2415
+ // The realtime module inserts its settings popover directly after the button it was handed, and
2416
+ // hard-codes the element id. A page can host two chat clients at once -- this one and the admin
2417
+ // widget -- so that id is not unique and getElementById would hand the widget the page's gear.
2418
+ // Each client finds its own next to its own button.
2419
+ voiceSettingsElement: function voiceSettingsElement() {
2420
+ var next = this.conversationButton && this.conversationButton.nextElementSibling;
2421
+ return next && next.id === 'realtime-audio-settings' ? next : null;
2422
+ },
2423
+ setVoiceSessionLive: function setVoiceSessionLive(live) {
2424
+ if (this.inputElement) {
2425
+ this.inputElement.hidden = live;
2426
+ }
2427
+ if (this.buttonElement) {
2428
+ this.buttonElement.hidden = live;
2429
+ }
2430
+
2431
+ // With the message box gone the button would otherwise sit at its natural width in an empty
2432
+ // row, so it takes the space the box left behind and reads as the thing now in charge. The
2433
+ // settings popover does not grow, so it stays at the right-hand end.
2434
+ if (this.conversationButton) {
2435
+ this.conversationButton.classList.toggle('flex-grow-1', live);
2436
+ }
2437
+
2438
+ // The gear is built by the realtime module next to its button, and only in realtime mode, so
2439
+ // it is absent on the cascade. It belongs to a live session rather than to the surface.
2440
+ var audioSettings = this.voiceSettingsElement();
2441
+ if (audioSettings) {
2442
+ audioSettings.hidden = !live;
2443
+ }
2444
+ var hint = this.realtimeHintElement;
2445
+ if (hint) {
2446
+ hint.classList.toggle('d-none', !live);
2447
+ }
2448
+ },
2402
2449
  toggleConversationMode: function toggleConversationMode() {
2403
2450
  if (this.realtimeEnabled) {
2404
2451
  // Realtime is driven by the shared module, which owns the button; this only exists for
@@ -2474,19 +2521,25 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
2474
2521
  },
2475
2522
  realtimeEnabled: true,
2476
2523
  // The conversation button doubles as the realtime button on this host; the module owns its
2477
- // click, label and state from here on. The text controls are handed over too so the module
2478
- // hides them: realtime is audio-only, and a message box that plays no part in a spoken
2479
- // conversation only invites the user to type into it.
2524
+ // click, label and state from here on.
2525
+ //
2526
+ // input and sendButton are deliberately not handed over. The module hides every control it
2527
+ // is given when realtime takes over, and typing has to stay available during a voice
2528
+ // conversation -- that is the whole feature.
2480
2529
  //
2481
- // conversationButton is deliberately not passed. It is the same element as the realtime
2530
+ // conversationButton is not handed over either. It is the same element as the realtime
2482
2531
  // button here, and the module hides the conversation button before showing the realtime
2483
2532
  // one -- naming it twice would ask it to hide the control it then has to show.
2484
2533
  selectors: {
2485
2534
  realtimeButton: config.conversationButtonElementSelector,
2486
- input: config.inputElementSelector,
2487
- sendButton: config.sendButtonElementSelector,
2488
2535
  micButton: config.micButtonElementSelector
2489
2536
  },
2537
+ // The module owns whether a session is live, so the message box follows its flag rather
2538
+ // than this app's copy of it. Losing one of these events used to leave the box hidden
2539
+ // under a button that had already gone back to "Start speaking".
2540
+ onSessionStateChanged: function onSessionStateChanged(active) {
2541
+ self.setVoiceSessionLive(active);
2542
+ },
2490
2543
  onActivate: function onActivate() {
2491
2544
  self.isConversationMode = true;
2492
2545
  self._conversationAssistantMessage = null;
@@ -3129,6 +3182,11 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
3129
3182
  // The shared realtime module owns the button (click, label, state) and adds the
3130
3183
  // voice settings popover next to it.
3131
3184
  this.setupRealtimeController(config);
3185
+
3186
+ // The module reveals the gear as soon as the surface enters realtime mode, but it
3187
+ // belongs to a live session. The watcher that would hide it only runs on a change,
3188
+ // so the idle state has to be stated once here.
3189
+ this.setVoiceSessionLive(false);
3132
3190
  } else {
3133
3191
  this.conversationButton.addEventListener('click', function () {
3134
3192
  _this23.toggleConversationMode();
@@ -3322,22 +3380,20 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
3322
3380
  });
3323
3381
  },
3324
3382
  isConversationMode: function isConversationMode(active) {
3325
- // Hide/show mic button.
3383
+ // The dictation microphone is hidden while a voice conversation runs: the conversation
3384
+ // already owns the microphone, and a second one would mean something different.
3326
3385
  if (this.micButton) {
3327
3386
  this.micButton.style.display = active ? 'none' : this.speechToTextEnabled ? '' : 'none';
3328
3387
  }
3329
3388
 
3330
- // Hide/show send button.
3331
- if (this.buttonElement) {
3332
- this.buttonElement.style.display = active ? 'none' : '';
3333
- }
3334
-
3335
- // Disable/enable textarea.
3336
- if (this.inputElement) {
3337
- this.inputElement.disabled = active;
3338
- if (active) {
3339
- this.inputElement.placeholder = '';
3340
- }
3389
+ // The message box and the send button give way to the voice settings while the conversation
3390
+ // runs, and come back when it ends. Both kinds of turn still land in the same thread; what
3391
+ // they cannot do is overlap.
3392
+ //
3393
+ // Only the cascade is driven from here. Realtime follows the module's own session flag
3394
+ // (onSessionStateChanged), so the two never disagree about whether a session is live.
3395
+ if (!this.realtimeEnabled) {
3396
+ this.setVoiceSessionLive(active);
3341
3397
  }
3342
3398
  }
3343
3399
  },
@@ -3450,6 +3506,7 @@ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMa
3450
3506
  chatMode: getAttributeValue(element, 'data-coreai-chat-mode'),
3451
3507
  micButtonElementSelector: getAttributeValue(element, 'data-coreai-chat-mic-button-element-selector'),
3452
3508
  conversationButtonElementSelector: getAttributeValue(element, 'data-coreai-chat-conversation-button-element-selector'),
3509
+ realtimeHintElementSelector: getAttributeValue(element, 'data-coreai-chat-realtime-hint-element-selector'),
3453
3510
  ttsVoiceName: getAttributeValue(element, 'data-coreai-chat-tts-voice-name'),
3454
3511
  realtimeVoiceName: getAttributeValue(element, 'data-coreai-chat-realtime-voice-name'),
3455
3512
  documentBarSelector: getAttributeValue(element, 'data-coreai-chat-document-bar-selector'),