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