@kodaris/krubble-app-components 1.0.100 → 1.0.101

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.
@@ -5626,6 +5626,24 @@ let KRChatbot = class KRChatbot extends i$3 {
5626
5626
  * in place; call requestUpdate() after changing it to re-render.
5627
5627
  */
5628
5628
  this._globalSkillPending = new Set();
5629
+ /**
5630
+ * The live Web Speech recognizer while dictating, else null. Not reactive —
5631
+ * `_isDictating` drives the UI; this is just the handle used to stop it.
5632
+ */
5633
+ this._speechRecognition = null;
5634
+ /**
5635
+ * The composer text as it stood when dictation started. Each `result` event
5636
+ * re-reports the whole utterance (final + interim), so the value is rebuilt as
5637
+ * this base + the transcript rather than appended to — appending would repeat
5638
+ * the interim text on every event.
5639
+ */
5640
+ this._speechBaseValue = '';
5641
+ /**
5642
+ * Transcript of utterances the recognizer has marked final this session.
5643
+ * Held separately because `results` is pruned in continuous mode on some
5644
+ * browsers; without this, earlier sentences would vanish mid-dictation.
5645
+ */
5646
+ this._speechFinalTranscript = '';
5629
5647
  // --- @property (public API) ---
5630
5648
  /**
5631
5649
  * Callback function for sending messages. Receives the user's message and
@@ -5775,6 +5793,8 @@ let KRChatbot = class KRChatbot extends i$3 {
5775
5793
  this._skillBrowseSource = 'org';
5776
5794
  /** Search text in the browse dialog, routed to the active tab's search. */
5777
5795
  this._skillBrowseQuery = '';
5796
+ /** True while the mic is live and speech is being transcribed into the composer. */
5797
+ this._isDictating = false;
5778
5798
  this._handleMouseMove = (e) => {
5779
5799
  if (this._isDragging) {
5780
5800
  let newLeft = this._dragStartLeft + (e.clientX - this._dragStartX);
@@ -5927,6 +5947,14 @@ let KRChatbot = class KRChatbot extends i$3 {
5927
5947
  }
5928
5948
  document.removeEventListener('mousemove', this._handleMouseMove);
5929
5949
  document.removeEventListener('mouseup', this._handleMouseUp);
5950
+ // Release the mic if the chatbot is torn down mid-dictation — otherwise the
5951
+ // recognizer keeps the browser's recording indicator lit with nothing to
5952
+ // transcribe into. abort() (not stop()) since the discarded text has nowhere to go.
5953
+ if (this._speechRecognition) {
5954
+ this._speechRecognition.abort();
5955
+ this._speechRecognition = null;
5956
+ this._isDictating = false;
5957
+ }
5930
5958
  // Release any object URLs still held by staged attachments.
5931
5959
  this._pendingConversationFiles.forEach((p) => {
5932
5960
  if (p.previewUrl) {
@@ -6533,6 +6561,92 @@ let KRChatbot = class KRChatbot extends i$3 {
6533
6561
  this._skillMenuOpened = true;
6534
6562
  this._searchInvocableSkills('');
6535
6563
  }
6564
+ /**
6565
+ * The mic button toggles dictation. Speech is transcribed straight into the
6566
+ * composer as editable text — the user still reviews and presses send, so a
6567
+ * misheard word is fixable and nothing is sent by voice alone.
6568
+ *
6569
+ * Recognition runs entirely in the browser's Web Speech API (Chrome/Edge send
6570
+ * audio to Google's service, Safari to Apple's). No audio reaches Kodaris, and
6571
+ * there's no backend call or cost on our side.
6572
+ */
6573
+ _handleMicClick() {
6574
+ // Second click stops. `stop()` flushes the pending utterance as a final
6575
+ // result, so the last words aren't lost; onend clears the state.
6576
+ if (this._speechRecognition) {
6577
+ this._speechRecognition.stop();
6578
+ return;
6579
+ }
6580
+ // Prefixed in Chrome/Edge/Safari; the unprefixed name is checked first for
6581
+ // whenever they drop the prefix. The mic button isn't rendered without one of
6582
+ // these, so reaching here unsupported means a mid-session change — bail quietly.
6583
+ const SpeechRecognitionCtor = window.SpeechRecognition || window.webkitSpeechRecognition;
6584
+ if (!SpeechRecognitionCtor) {
6585
+ return;
6586
+ }
6587
+ const recognition = new SpeechRecognitionCtor();
6588
+ // Keep listening through natural pauses — a chat message is often several
6589
+ // sentences, and the default stops at the first one.
6590
+ recognition.continuous = true;
6591
+ // Show words as they're recognized rather than only at end of utterance.
6592
+ recognition.interimResults = true;
6593
+ // `lang` is deliberately left unset so the recognizer follows the page/OS
6594
+ // language, which is correct for every consumer of this component.
6595
+ // Dictation ADDS to whatever is already typed rather than replacing it, so
6596
+ // the user can type half a message and speak the rest. The separating space
6597
+ // is part of the base so the transcript never runs into the existing word.
6598
+ this._speechBaseValue = this._inputValue;
6599
+ if (this._speechBaseValue && !this._speechBaseValue.endsWith(' ')) {
6600
+ this._speechBaseValue += ' ';
6601
+ }
6602
+ this._speechFinalTranscript = '';
6603
+ recognition.onresult = (e) => {
6604
+ // Everything from resultIndex on is new since the last event: finalized
6605
+ // pieces move to _speechFinalTranscript permanently, while non-final ones
6606
+ // are rebuilt each event because the browser revises its guess as it hears
6607
+ // more. Rebuilding the whole value (base + final + interim) is what keeps
6608
+ // those revisions from stacking up as duplicated text.
6609
+ let interim = '';
6610
+ for (let i = e.resultIndex; i < e.results.length; i++) {
6611
+ if (e.results[i].isFinal) {
6612
+ this._speechFinalTranscript += e.results[i][0].transcript;
6613
+ }
6614
+ else {
6615
+ interim += e.results[i][0].transcript;
6616
+ }
6617
+ }
6618
+ this._inputValue = this._speechBaseValue + this._speechFinalTranscript + interim;
6619
+ this._saveState();
6620
+ // Grow the textarea as dictated text wraps — the same clamp the typing path
6621
+ // uses, since @input doesn't fire for programmatic value changes.
6622
+ if (this._inputEl) {
6623
+ this._inputEl.style.height = 'auto';
6624
+ this._inputEl.style.height = Math.min(this._inputEl.scrollHeight, 120) + 'px';
6625
+ }
6626
+ };
6627
+ recognition.onerror = (e) => {
6628
+ // Denied mic permission is the one error worth explaining — the user has to
6629
+ // fix it in browser settings, and the button would otherwise look broken.
6630
+ if (e.error === 'not-allowed' || e.error === 'service-not-allowed') {
6631
+ this._error = 'Microphone access is blocked. Allow it in your browser settings to use voice input.';
6632
+ return;
6633
+ }
6634
+ // `no-speech` (silence) and `aborted` (user stopped) are normal; anything
6635
+ // else is a transient service hiccup. onend still runs, so the mic resets.
6636
+ };
6637
+ // Fires on stop(), on error, and when the browser ends recognition on its own
6638
+ // (e.g. a long silence) — the single place the live state is torn down.
6639
+ recognition.onend = () => {
6640
+ this._speechRecognition = null;
6641
+ this._isDictating = false;
6642
+ };
6643
+ // start() is what triggers the browser's mic permission prompt.
6644
+ this._speechRecognition = recognition;
6645
+ this._isDictating = true;
6646
+ this._error = '';
6647
+ recognition.start();
6648
+ this._inputEl?.focus();
6649
+ }
6536
6650
  /** Insert `/code ` into the composer and focus it, ready for the message. */
6537
6651
  _handleSkillSelect(skill) {
6538
6652
  // A global skill must be installed before it can be chosen — inserting /code
@@ -6803,6 +6917,13 @@ let KRChatbot = class KRChatbot extends i$3 {
6803
6917
  previewUrl: p.previewUrl,
6804
6918
  }));
6805
6919
  }
6920
+ // The message is on its way, so stop the mic if it's still live — otherwise
6921
+ // it would keep transcribing into the now-cleared composer. abort() rather
6922
+ // than stop() because a trailing pending utterance belongs to the message
6923
+ // just sent; flushing it would drop stray words into the next one.
6924
+ if (this._speechRecognition) {
6925
+ this._speechRecognition.abort();
6926
+ }
6806
6927
  this.messages = [...this.messages, userMessage];
6807
6928
  this._inputValue = '';
6808
6929
  this._saveState();
@@ -7875,6 +7996,31 @@ let KRChatbot = class KRChatbot extends i$3 {
7875
7996
  @keydown=${this._handleKeyDown}
7876
7997
  @paste=${this._handlePaste}
7877
7998
  ></textarea>
7999
+ ${window.SpeechRecognition || window.webkitSpeechRecognition ? b `
8000
+ <button
8001
+ class=${e({
8002
+ mic: true,
8003
+ 'mic--recording': this._isDictating,
8004
+ })}
8005
+ title=${this._isDictating ? 'Stop dictation' : 'Dictate a message'}
8006
+ @click=${this._handleMicClick}
8007
+ >
8008
+ <svg
8009
+ xmlns="http://www.w3.org/2000/svg"
8010
+ viewBox="0 0 24 24"
8011
+ fill="none"
8012
+ stroke="currentColor"
8013
+ stroke-width="1.75"
8014
+ stroke-linecap="round"
8015
+ stroke-linejoin="round"
8016
+ class="mic-icon"
8017
+ >
8018
+ <path d="M12 15a3 3 0 0 0 3-3V6a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3Z"/>
8019
+ <path d="M19 11a7 7 0 0 1-14 0"/>
8020
+ <path d="M12 18v3"/>
8021
+ </svg>
8022
+ </button>
8023
+ ` : A}
7878
8024
  ${this._isStreaming ? b `
7879
8025
  <button
7880
8026
  class="stop"
@@ -8718,6 +8864,51 @@ KRChatbot.styles = i$6 `
8718
8864
  color: #b0b0b0;
8719
8865
  }
8720
8866
 
8867
+ .mic {
8868
+ width: 34px;
8869
+ height: 34px;
8870
+ border: none;
8871
+ background: transparent;
8872
+ color: var(--kr-chatbot-text);
8873
+ border-radius: 50%;
8874
+ cursor: pointer;
8875
+ display: flex;
8876
+ align-items: center;
8877
+ justify-content: center;
8878
+ flex-shrink: 0;
8879
+ transition: background 0.15s, color 0.15s;
8880
+ }
8881
+
8882
+ .mic:hover {
8883
+ background: #f8f9fc;
8884
+ }
8885
+
8886
+ /* Live dictation: red mic on a tinted circle, pulsing so it's clear the
8887
+ microphone is still open even when the user has stopped speaking. */
8888
+ .mic--recording {
8889
+ color: var(--kr-chatbot-error-text);
8890
+ background: var(--kr-chatbot-error-bg);
8891
+ animation: chatbot-mic-pulse 1.6s ease-in-out infinite;
8892
+ }
8893
+
8894
+ .mic--recording:hover {
8895
+ background: var(--kr-chatbot-error-bg);
8896
+ }
8897
+
8898
+ .mic-icon {
8899
+ width: 18px;
8900
+ height: 18px;
8901
+ }
8902
+
8903
+ @keyframes chatbot-mic-pulse {
8904
+ 0%, 100% {
8905
+ opacity: 1;
8906
+ }
8907
+ 50% {
8908
+ opacity: 0.55;
8909
+ }
8910
+ }
8911
+
8721
8912
  .send {
8722
8913
  width: 34px;
8723
8914
  height: 34px;
@@ -9345,6 +9536,9 @@ __decorate([
9345
9536
  __decorate([
9346
9537
  r()
9347
9538
  ], KRChatbot.prototype, "_skillBrowseQuery", void 0);
9539
+ __decorate([
9540
+ r()
9541
+ ], KRChatbot.prototype, "_isDictating", void 0);
9348
9542
  __decorate([
9349
9543
  e$3('.panel')
9350
9544
  ], KRChatbot.prototype, "_panelEl", void 0);