@speechos/client 0.2.5 → 0.2.6

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { state, events, getBackend, getConfig, setConfig, updateUserId } from '@speechos/core';
1
+ import { state, events, getConfig, getBackend, setConfig, updateUserId } from '@speechos/core';
2
2
  export { DEFAULT_HOST, events, getConfig, livekit, resetConfig, setConfig, state } from '@speechos/core';
3
3
 
4
4
  /**
@@ -1757,7 +1757,8 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
1757
1757
  this.activeAction = null;
1758
1758
  this.editPreviewText = "";
1759
1759
  this.errorMessage = null;
1760
- this.commandFeedback = null;
1760
+ this.actionFeedback = null;
1761
+ this.showNoAudioWarning = false;
1761
1762
  }
1762
1763
  static { this.styles = [
1763
1764
  themeStyles,
@@ -2347,8 +2348,9 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2347
2348
  background-position: center;
2348
2349
  }
2349
2350
 
2350
- /* Command feedback badge - no match state (neutral gray) */
2351
- .status-label.command-none {
2351
+ /* Command/edit feedback badge - no match/empty state (neutral gray) */
2352
+ .status-label.command-none,
2353
+ .status-label.edit-empty {
2352
2354
  background: #4b5563;
2353
2355
  box-shadow: 0 4px 12px rgba(75, 85, 99, 0.3);
2354
2356
  animation: command-feedback-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)
@@ -2505,6 +2507,60 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2505
2507
  border-color: rgba(255, 255, 255, 0.5);
2506
2508
  }
2507
2509
 
2510
+ /* No audio warning banner */
2511
+ .no-audio-warning {
2512
+ position: absolute;
2513
+ bottom: 120px; /* Above button and waveform visualizer */
2514
+ left: 50%;
2515
+ transform: translateX(-50%) translateY(8px);
2516
+ display: flex;
2517
+ align-items: center;
2518
+ gap: 8px;
2519
+ padding: 10px 14px;
2520
+ border-radius: 12px;
2521
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
2522
+ box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
2523
+ transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
2524
+ pointer-events: none;
2525
+ opacity: 0;
2526
+ white-space: nowrap;
2527
+ }
2528
+
2529
+ .no-audio-warning.visible {
2530
+ opacity: 1;
2531
+ transform: translateX(-50%) translateY(0);
2532
+ pointer-events: auto;
2533
+ }
2534
+
2535
+ .no-audio-warning .warning-icon {
2536
+ flex-shrink: 0;
2537
+ color: white;
2538
+ }
2539
+
2540
+ .no-audio-warning .warning-text {
2541
+ font-size: 13px;
2542
+ font-weight: 500;
2543
+ color: white;
2544
+ }
2545
+
2546
+ .no-audio-warning .settings-link {
2547
+ background: rgba(255, 255, 255, 0.2);
2548
+ border: 1px solid rgba(255, 255, 255, 0.3);
2549
+ border-radius: 6px;
2550
+ padding: 4px 10px;
2551
+ font-size: 12px;
2552
+ font-weight: 600;
2553
+ color: white;
2554
+ cursor: pointer;
2555
+ transition: all 0.15s;
2556
+ white-space: nowrap;
2557
+ }
2558
+
2559
+ .no-audio-warning .settings-link:hover {
2560
+ background: rgba(255, 255, 255, 0.3);
2561
+ border-color: rgba(255, 255, 255, 0.5);
2562
+ }
2563
+
2508
2564
  /* Mobile styles - 30% larger */
2509
2565
  @media (max-width: 768px) and (hover: none) {
2510
2566
  .mic-button {
@@ -2634,6 +2690,21 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2634
2690
  padding: 8px 14px;
2635
2691
  font-size: 14px;
2636
2692
  }
2693
+
2694
+ .no-audio-warning {
2695
+ padding: 12px 16px;
2696
+ gap: 10px;
2697
+ bottom: 145px; /* Above button and waveform on mobile */
2698
+ }
2699
+
2700
+ .no-audio-warning .warning-text {
2701
+ font-size: 15px;
2702
+ }
2703
+
2704
+ .no-audio-warning .settings-link {
2705
+ padding: 6px 12px;
2706
+ font-size: 14px;
2707
+ }
2637
2708
  }
2638
2709
  `,
2639
2710
  ]; }
@@ -2691,6 +2762,14 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2691
2762
  composed: true,
2692
2763
  }));
2693
2764
  }
2765
+ handleOpenSettings(e) {
2766
+ e.stopPropagation();
2767
+ e.preventDefault();
2768
+ this.dispatchEvent(new CustomEvent("open-settings", {
2769
+ bubbles: true,
2770
+ composed: true,
2771
+ }));
2772
+ }
2694
2773
  getButtonClass() {
2695
2774
  const classes = ["mic-button"];
2696
2775
  if (this.expanded && this.recordingState === "idle") {
@@ -2775,13 +2854,16 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2775
2854
  }
2776
2855
  return this.recordingState;
2777
2856
  }
2778
- getCommandFeedbackLabel() {
2779
- if (this.commandFeedback === "success") {
2857
+ getActionFeedbackLabel() {
2858
+ if (this.actionFeedback === "command-success") {
2780
2859
  return "Got it!";
2781
2860
  }
2782
- if (this.commandFeedback === "none") {
2861
+ if (this.actionFeedback === "command-none") {
2783
2862
  return "No command matched";
2784
2863
  }
2864
+ if (this.actionFeedback === "edit-empty") {
2865
+ return "Couldn't understand edit";
2866
+ }
2785
2867
  return "";
2786
2868
  }
2787
2869
  render() {
@@ -2791,9 +2873,9 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2791
2873
  const showSiriEdit = this.recordingState === "processing" && this.activeAction === "edit";
2792
2874
  const statusLabel = this.getStatusLabel();
2793
2875
  const showVisualizer = this.shouldShowVisualizer();
2794
- // Show status label during recording (either visualizer or edit text) OR command feedback
2795
- const showCommandFeedback = this.recordingState === "idle" && this.commandFeedback !== null;
2796
- const showStatus = this.recordingState === "recording" || showCommandFeedback;
2876
+ // Show status label during recording (either visualizer or edit text) OR action feedback
2877
+ const showActionFeedback = this.recordingState === "idle" && this.actionFeedback !== null;
2878
+ const showStatus = this.recordingState === "recording" || showActionFeedback;
2797
2879
  const showCancel = this.recordingState === "connecting" ||
2798
2880
  this.recordingState === "recording" ||
2799
2881
  this.recordingState === "processing";
@@ -2832,6 +2914,35 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2832
2914
  `
2833
2915
  : ""}
2834
2916
 
2917
+ <div
2918
+ class="no-audio-warning ${this.showNoAudioWarning &&
2919
+ this.recordingState === "recording"
2920
+ ? "visible"
2921
+ : ""}"
2922
+ >
2923
+ <svg
2924
+ class="warning-icon"
2925
+ width="16"
2926
+ height="16"
2927
+ viewBox="0 0 24 24"
2928
+ fill="none"
2929
+ stroke="currentColor"
2930
+ stroke-width="2"
2931
+ stroke-linecap="round"
2932
+ stroke-linejoin="round"
2933
+ >
2934
+ <path
2935
+ d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
2936
+ />
2937
+ <line x1="12" y1="9" x2="12" y2="13" />
2938
+ <line x1="12" y1="17" x2="12.01" y2="17" />
2939
+ </svg>
2940
+ <span class="warning-text">We're not hearing anything</span>
2941
+ <button class="settings-link" @click="${this.handleOpenSettings}">
2942
+ Check Settings
2943
+ </button>
2944
+ </div>
2945
+
2835
2946
  <button
2836
2947
  class="${this.getButtonClass()}"
2837
2948
  @click="${this.handleClick}"
@@ -2844,14 +2955,14 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2844
2955
  </button>
2845
2956
 
2846
2957
  <span
2847
- class="status-label ${showStatus ? "visible" : ""} ${showCommandFeedback
2848
- ? `command-${this.commandFeedback}`
2958
+ class="status-label ${showStatus ? "visible" : ""} ${showActionFeedback
2959
+ ? this.actionFeedback
2849
2960
  : showVisualizer
2850
2961
  ? "visualizer"
2851
2962
  : this.getStatusClass()}"
2852
2963
  >
2853
- ${showCommandFeedback
2854
- ? this.getCommandFeedbackLabel()
2964
+ ${showActionFeedback
2965
+ ? this.getActionFeedbackLabel()
2855
2966
  : showVisualizer
2856
2967
  ? b `<speechos-audio-visualizer
2857
2968
  ?active="${showVisualizer}"
@@ -2899,7 +3010,10 @@ __decorate([
2899
3010
  ], SpeechOSMicButton.prototype, "errorMessage", void 0);
2900
3011
  __decorate([
2901
3012
  n({ type: String })
2902
- ], SpeechOSMicButton.prototype, "commandFeedback", void 0);
3013
+ ], SpeechOSMicButton.prototype, "actionFeedback", void 0);
3014
+ __decorate([
3015
+ n({ type: Boolean })
3016
+ ], SpeechOSMicButton.prototype, "showNoAudioWarning", void 0);
2903
3017
  SpeechOSMicButton = __decorate([
2904
3018
  t$1("speechos-mic-button")
2905
3019
  ], SpeechOSMicButton);
@@ -6273,15 +6387,27 @@ var SpeechOSWidget_1;
6273
6387
  * duration so users can see the visual feedback before transitioning to recording.
6274
6388
  */
6275
6389
  const MIN_CONNECTING_ANIMATION_MS = 200;
6390
+ /**
6391
+ * Time to wait for a transcription event before showing the "no audio" warning (in milliseconds).
6392
+ * If no transcription:interim event is received within this time during recording,
6393
+ * it indicates the server isn't receiving/processing audio.
6394
+ */
6395
+ const NO_AUDIO_WARNING_TIMEOUT_MS = 5000;
6396
+ /**
6397
+ * Number of consecutive actions with empty results before showing warning on next action.
6398
+ */
6399
+ const CONSECUTIVE_NO_AUDIO_THRESHOLD = 2;
6276
6400
  let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6277
6401
  constructor() {
6278
6402
  super(...arguments);
6279
6403
  this.widgetState = state.getState();
6280
6404
  this.settingsOpen = false;
6405
+ this.settingsOpenFromWarning = false;
6281
6406
  this.dictationModalOpen = false;
6282
6407
  this.dictationModalText = "";
6283
6408
  this.editHelpModalOpen = false;
6284
- this.commandFeedback = null;
6409
+ this.actionFeedback = null;
6410
+ this.showNoAudioWarning = false;
6285
6411
  this.dictationTargetElement = null;
6286
6412
  this.editTargetElement = null;
6287
6413
  this.dictationCursorStart = null;
@@ -6293,7 +6419,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6293
6419
  this.modalElement = null;
6294
6420
  this.dictationModalElement = null;
6295
6421
  this.editHelpModalElement = null;
6296
- this.commandFeedbackTimeout = null;
6422
+ this.actionFeedbackTimeout = null;
6297
6423
  this.customPosition = null;
6298
6424
  this.isDragging = false;
6299
6425
  this.dragStartPos = null;
@@ -6303,6 +6429,11 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6303
6429
  this.suppressNextClick = false;
6304
6430
  this.boundViewportResizeHandler = null;
6305
6431
  this.boundScrollHandler = null;
6432
+ // No-audio warning state tracking
6433
+ this.consecutiveNoAudioActions = 0;
6434
+ this.transcriptionReceived = false;
6435
+ this.noAudioWarningTimeout = null;
6436
+ this.transcriptionInterimUnsubscribe = null;
6306
6437
  }
6307
6438
  static { SpeechOSWidget_1 = this; }
6308
6439
  static { this.styles = [
@@ -6385,6 +6516,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6385
6516
  this.modalElement = document.createElement("speechos-settings-modal");
6386
6517
  this.modalElement.addEventListener("modal-close", () => {
6387
6518
  this.settingsOpen = false;
6519
+ this.settingsOpenFromWarning = false;
6388
6520
  });
6389
6521
  document.body.appendChild(this.modalElement);
6390
6522
  // Mount dictation output modal
@@ -6400,7 +6532,17 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6400
6532
  });
6401
6533
  document.body.appendChild(this.editHelpModalElement);
6402
6534
  this.stateUnsubscribe = state.subscribe((newState) => {
6403
- if (!newState.isVisible || !newState.isExpanded) {
6535
+ if (!newState.isVisible) {
6536
+ if (getConfig().debug && this.settingsOpen) {
6537
+ console.log("[SpeechOS] Closing settings modal: widget hidden");
6538
+ }
6539
+ this.settingsOpen = false;
6540
+ this.settingsOpenFromWarning = false;
6541
+ }
6542
+ else if (!newState.isExpanded && !this.settingsOpenFromWarning) {
6543
+ if (getConfig().debug && this.settingsOpen) {
6544
+ console.log("[SpeechOS] Closing settings modal: widget collapsed");
6545
+ }
6404
6546
  this.settingsOpen = false;
6405
6547
  }
6406
6548
  // Clear custom position when focused element changes (re-anchor to new element)
@@ -6446,9 +6588,9 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6446
6588
  this.editHelpModalElement.remove();
6447
6589
  this.editHelpModalElement = null;
6448
6590
  }
6449
- if (this.commandFeedbackTimeout) {
6450
- clearTimeout(this.commandFeedbackTimeout);
6451
- this.commandFeedbackTimeout = null;
6591
+ if (this.actionFeedbackTimeout) {
6592
+ clearTimeout(this.actionFeedbackTimeout);
6593
+ this.actionFeedbackTimeout = null;
6452
6594
  }
6453
6595
  if (this.stateUnsubscribe) {
6454
6596
  this.stateUnsubscribe();
@@ -6476,6 +6618,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6476
6618
  window.removeEventListener("scroll", this.boundScrollHandler);
6477
6619
  this.boundScrollHandler = null;
6478
6620
  }
6621
+ this.cleanupNoAudioWarningTracking();
6479
6622
  }
6480
6623
  updated(changedProperties) {
6481
6624
  if (changedProperties.has("settingsOpen") && this.modalElement) {
@@ -6653,7 +6796,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6653
6796
  }
6654
6797
  if (this.widgetState.recordingState === "idle") {
6655
6798
  // Clear command feedback on any mic click
6656
- this.clearCommandFeedback();
6799
+ this.clearActionFeedback();
6657
6800
  // If we're expanding, prefetch the token to reduce latency when user selects an action
6658
6801
  if (!this.widgetState.isExpanded) {
6659
6802
  // Fire and forget - we don't need to wait for this (LiveKit only)
@@ -6674,6 +6817,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6674
6817
  }
6675
6818
  }
6676
6819
  async handleStopRecording() {
6820
+ // Clean up no-audio warning tracking
6821
+ this.cleanupNoAudioWarningTracking();
6677
6822
  if (this.widgetState.activeAction === "edit") {
6678
6823
  await this.handleStopEdit();
6679
6824
  }
@@ -6685,6 +6830,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6685
6830
  const backend = getBackend();
6686
6831
  try {
6687
6832
  const transcription = await this.withMinDisplayTime(backend.stopVoiceSession(), 300);
6833
+ // Track result for consecutive failure detection
6834
+ this.trackActionResult(!!transcription);
6688
6835
  if (transcription) {
6689
6836
  // Check if we have a target element to insert into
6690
6837
  if (this.dictationTargetElement) {
@@ -6704,6 +6851,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6704
6851
  backend.startAutoRefresh?.();
6705
6852
  }
6706
6853
  catch (error) {
6854
+ // Track as failed result
6855
+ this.trackActionResult(false);
6707
6856
  const errorMessage = error instanceof Error ? error.message : "Failed to transcribe audio";
6708
6857
  if (errorMessage !== "Disconnected") {
6709
6858
  state.setError(errorMessage);
@@ -6713,6 +6862,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6713
6862
  }
6714
6863
  }
6715
6864
  async handleCancelOperation() {
6865
+ // Clean up no-audio warning tracking
6866
+ this.cleanupNoAudioWarningTracking();
6716
6867
  await getBackend().disconnect();
6717
6868
  if (this.widgetState.recordingState === "error") {
6718
6869
  state.clearError();
@@ -6742,7 +6893,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6742
6893
  }
6743
6894
  }
6744
6895
  handleCloseWidget() {
6745
- this.clearCommandFeedback();
6896
+ this.clearActionFeedback();
6746
6897
  getBackend().stopAutoRefresh?.();
6747
6898
  state.hide();
6748
6899
  }
@@ -6871,7 +7022,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6871
7022
  handleActionSelect(event) {
6872
7023
  const { action } = event.detail;
6873
7024
  // Clear any existing command feedback when a new action is selected
6874
- this.clearCommandFeedback();
7025
+ this.clearActionFeedback();
6875
7026
  state.setActiveAction(action);
6876
7027
  if (action === "dictate") {
6877
7028
  this.startDictation();
@@ -6934,10 +7085,12 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6934
7085
  if (remainingDelay > 0) {
6935
7086
  setTimeout(() => {
6936
7087
  state.setRecordingState("recording");
7088
+ this.startNoAudioWarningTracking();
6937
7089
  }, remainingDelay);
6938
7090
  }
6939
7091
  else {
6940
7092
  state.setRecordingState("recording");
7093
+ this.startNoAudioWarningTracking();
6941
7094
  }
6942
7095
  },
6943
7096
  });
@@ -6945,7 +7098,10 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6945
7098
  catch (error) {
6946
7099
  const errorMessage = error instanceof Error ? error.message : "Connection failed";
6947
7100
  if (errorMessage !== "Disconnected") {
6948
- state.setError(`Failed to connect: ${errorMessage}`);
7101
+ // Only set error if not already in error state (error event may have already set it)
7102
+ if (this.widgetState.recordingState !== "error") {
7103
+ state.setError(`Failed to connect: ${errorMessage}`);
7104
+ }
6949
7105
  await backend.disconnect();
6950
7106
  }
6951
7107
  }
@@ -7000,10 +7156,12 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7000
7156
  if (remainingDelay > 0) {
7001
7157
  setTimeout(() => {
7002
7158
  state.setRecordingState("recording");
7159
+ this.startNoAudioWarningTracking();
7003
7160
  }, remainingDelay);
7004
7161
  }
7005
7162
  else {
7006
7163
  state.setRecordingState("recording");
7164
+ this.startNoAudioWarningTracking();
7007
7165
  }
7008
7166
  },
7009
7167
  });
@@ -7011,7 +7169,10 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7011
7169
  catch (error) {
7012
7170
  const errorMessage = error instanceof Error ? error.message : "Connection failed";
7013
7171
  if (errorMessage !== "Disconnected") {
7014
- state.setError(`Failed to connect: ${errorMessage}`);
7172
+ // Only set error if not already in error state (error event may have already set it)
7173
+ if (this.widgetState.recordingState !== "error") {
7174
+ state.setError(`Failed to connect: ${errorMessage}`);
7175
+ }
7015
7176
  await backend.disconnect();
7016
7177
  }
7017
7178
  }
@@ -7022,12 +7183,30 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7022
7183
  const backend = getBackend();
7023
7184
  try {
7024
7185
  const editedText = await this.withMinDisplayTime(backend.requestEditText(originalContent), 300);
7186
+ // Check if server returned no change (couldn't understand edit)
7187
+ const noChange = editedText.trim() === originalContent.trim();
7188
+ if (noChange) {
7189
+ this.trackActionResult(false);
7190
+ this.showActionFeedback("edit-empty");
7191
+ state.completeRecording();
7192
+ this.editTargetElement = null;
7193
+ this.editSelectionStart = null;
7194
+ this.editSelectionEnd = null;
7195
+ this.editSelectedText = "";
7196
+ backend.disconnect().catch(() => { });
7197
+ backend.startAutoRefresh?.();
7198
+ return;
7199
+ }
7200
+ // Track result - got a meaningful change
7201
+ this.trackActionResult(true);
7025
7202
  this.applyEdit(editedText);
7026
7203
  backend.disconnect().catch(() => { });
7027
7204
  // Start auto-refresh to keep token fresh for subsequent commands (LiveKit only)
7028
7205
  backend.startAutoRefresh?.();
7029
7206
  }
7030
7207
  catch (error) {
7208
+ // Track as failed result
7209
+ this.trackActionResult(false);
7031
7210
  const errorMessage = error instanceof Error ? error.message : "Failed to apply edit";
7032
7211
  if (errorMessage !== "Disconnected") {
7033
7212
  state.setError(errorMessage);
@@ -7053,10 +7232,12 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7053
7232
  if (remainingDelay > 0) {
7054
7233
  setTimeout(() => {
7055
7234
  state.setRecordingState("recording");
7235
+ this.startNoAudioWarningTracking();
7056
7236
  }, remainingDelay);
7057
7237
  }
7058
7238
  else {
7059
7239
  state.setRecordingState("recording");
7240
+ this.startNoAudioWarningTracking();
7060
7241
  }
7061
7242
  },
7062
7243
  });
@@ -7064,7 +7245,10 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7064
7245
  catch (error) {
7065
7246
  const errorMessage = error instanceof Error ? error.message : "Connection failed";
7066
7247
  if (errorMessage !== "Disconnected") {
7067
- state.setError(`Failed to connect: ${errorMessage}`);
7248
+ // Only set error if not already in error state (error event may have already set it)
7249
+ if (this.widgetState.recordingState !== "error") {
7250
+ state.setError(`Failed to connect: ${errorMessage}`);
7251
+ }
7068
7252
  await backend.disconnect();
7069
7253
  }
7070
7254
  }
@@ -7076,6 +7260,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7076
7260
  const backend = getBackend();
7077
7261
  try {
7078
7262
  const result = await this.withMinDisplayTime(backend.requestCommand(commands), 300);
7263
+ // Track result - null result means no command matched (possibly no audio)
7264
+ this.trackActionResult(result !== null);
7079
7265
  // Get input text from the backend if available
7080
7266
  const inputText = backend.getLastInputText?.();
7081
7267
  // Save to transcript store
@@ -7093,12 +7279,14 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7093
7279
  // Keep widget visible but collapsed (just mic button, no action bubbles)
7094
7280
  state.setState({ isExpanded: false });
7095
7281
  // Show command feedback
7096
- this.showCommandFeedback(result ? "success" : "none");
7282
+ this.showActionFeedback(result ? "command-success" : "command-none");
7097
7283
  backend.disconnect().catch(() => { });
7098
7284
  // Start auto-refresh to keep token fresh for subsequent commands (LiveKit only)
7099
7285
  backend.startAutoRefresh?.();
7100
7286
  }
7101
7287
  catch (error) {
7288
+ // Track as failed result
7289
+ this.trackActionResult(false);
7102
7290
  const errorMessage = error instanceof Error ? error.message : "Failed to process command";
7103
7291
  if (errorMessage !== "Disconnected") {
7104
7292
  state.setError(errorMessage);
@@ -7106,24 +7294,110 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7106
7294
  }
7107
7295
  }
7108
7296
  }
7109
- showCommandFeedback(feedback) {
7110
- this.commandFeedback = feedback;
7297
+ showActionFeedback(feedback) {
7298
+ this.actionFeedback = feedback;
7111
7299
  // Clear any existing timeout
7112
- if (this.commandFeedbackTimeout) {
7113
- clearTimeout(this.commandFeedbackTimeout);
7300
+ if (this.actionFeedbackTimeout) {
7301
+ clearTimeout(this.actionFeedbackTimeout);
7114
7302
  }
7115
7303
  // Auto-dismiss after 4 seconds
7116
- this.commandFeedbackTimeout = window.setTimeout(() => {
7117
- this.commandFeedback = null;
7118
- this.commandFeedbackTimeout = null;
7304
+ this.actionFeedbackTimeout = window.setTimeout(() => {
7305
+ this.actionFeedback = null;
7306
+ this.actionFeedbackTimeout = null;
7119
7307
  }, 4000);
7120
7308
  }
7121
- clearCommandFeedback() {
7122
- if (this.commandFeedbackTimeout) {
7123
- clearTimeout(this.commandFeedbackTimeout);
7124
- this.commandFeedbackTimeout = null;
7309
+ clearActionFeedback() {
7310
+ if (this.actionFeedbackTimeout) {
7311
+ clearTimeout(this.actionFeedbackTimeout);
7312
+ this.actionFeedbackTimeout = null;
7125
7313
  }
7126
- this.commandFeedback = null;
7314
+ this.actionFeedback = null;
7315
+ }
7316
+ /**
7317
+ * Start tracking for no-audio warning when recording begins.
7318
+ */
7319
+ startNoAudioWarningTracking() {
7320
+ this.transcriptionReceived = false;
7321
+ this.showNoAudioWarning = false;
7322
+ // If we had consecutive failures, show warning immediately
7323
+ if (this.consecutiveNoAudioActions >= CONSECUTIVE_NO_AUDIO_THRESHOLD) {
7324
+ this.showNoAudioWarning = true;
7325
+ }
7326
+ // Start timeout - if no transcription within 5s, show warning
7327
+ this.noAudioWarningTimeout = window.setTimeout(() => {
7328
+ if (!this.transcriptionReceived &&
7329
+ this.widgetState.recordingState === "recording") {
7330
+ this.showNoAudioWarning = true;
7331
+ }
7332
+ }, NO_AUDIO_WARNING_TIMEOUT_MS);
7333
+ // Subscribe to transcription:interim events
7334
+ this.transcriptionInterimUnsubscribe = events.on("transcription:interim", () => {
7335
+ this.transcriptionReceived = true;
7336
+ if (this.showNoAudioWarning) {
7337
+ this.showNoAudioWarning = false;
7338
+ }
7339
+ });
7340
+ }
7341
+ /**
7342
+ * Clean up no-audio warning tracking when recording stops.
7343
+ */
7344
+ cleanupNoAudioWarningTracking() {
7345
+ if (this.noAudioWarningTimeout !== null) {
7346
+ clearTimeout(this.noAudioWarningTimeout);
7347
+ this.noAudioWarningTimeout = null;
7348
+ }
7349
+ if (this.transcriptionInterimUnsubscribe) {
7350
+ this.transcriptionInterimUnsubscribe();
7351
+ this.transcriptionInterimUnsubscribe = null;
7352
+ }
7353
+ this.showNoAudioWarning = false;
7354
+ }
7355
+ /**
7356
+ * Track the result of an action for consecutive failure detection.
7357
+ */
7358
+ trackActionResult(hasContent) {
7359
+ if (hasContent) {
7360
+ this.consecutiveNoAudioActions = 0;
7361
+ }
7362
+ else {
7363
+ this.consecutiveNoAudioActions++;
7364
+ }
7365
+ }
7366
+ /**
7367
+ * Handle opening settings from the no-audio warning.
7368
+ * Stops the current dictation session immediately, then opens settings.
7369
+ */
7370
+ async handleOpenSettingsFromWarning() {
7371
+ if (getConfig().debug) {
7372
+ console.log("[SpeechOS] No-audio settings link clicked");
7373
+ }
7374
+ // Clean up no-audio warning tracking first
7375
+ this.cleanupNoAudioWarningTracking();
7376
+ // Keep settings open even if widget collapses
7377
+ this.settingsOpenFromWarning = true;
7378
+ // Stop audio capture and disconnect immediately (don't wait for transcription)
7379
+ // Kick this off before opening settings so audio stops fast, but don't block UI.
7380
+ const disconnectPromise = getBackend().disconnect().catch((error) => {
7381
+ if (getConfig().debug) {
7382
+ console.log("[SpeechOS] Disconnect failed while opening settings", error);
7383
+ }
7384
+ });
7385
+ // Update UI state to idle
7386
+ state.cancelRecording();
7387
+ // Clear target elements
7388
+ this.dictationTargetElement = null;
7389
+ this.editTargetElement = null;
7390
+ this.dictationCursorStart = null;
7391
+ this.dictationCursorEnd = null;
7392
+ this.editSelectionStart = null;
7393
+ this.editSelectionEnd = null;
7394
+ this.editSelectedText = "";
7395
+ // Open settings modal
7396
+ this.settingsOpen = true;
7397
+ if (getConfig().debug) {
7398
+ console.log("[SpeechOS] Settings modal opened from no-audio warning");
7399
+ }
7400
+ await disconnectPromise;
7127
7401
  }
7128
7402
  supportsSelection(element) {
7129
7403
  if (element.tagName.toLowerCase() === "textarea") {
@@ -7242,12 +7516,14 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7242
7516
  activeAction="${this.widgetState.activeAction || ""}"
7243
7517
  editPreviewText="${this.editSelectedText}"
7244
7518
  errorMessage="${this.widgetState.errorMessage || ""}"
7245
- .commandFeedback="${this.commandFeedback}"
7519
+ .actionFeedback="${this.actionFeedback}"
7520
+ ?showNoAudioWarning="${this.showNoAudioWarning}"
7246
7521
  @mic-click="${this.handleMicClick}"
7247
7522
  @stop-recording="${this.handleStopRecording}"
7248
7523
  @cancel-operation="${this.handleCancelOperation}"
7249
7524
  @retry-connection="${this.handleRetryConnection}"
7250
7525
  @close-widget="${this.handleCloseWidget}"
7526
+ @open-settings="${this.handleOpenSettingsFromWarning}"
7251
7527
  ></speechos-mic-button>
7252
7528
  </div>
7253
7529
  </div>
@@ -7271,7 +7547,10 @@ __decorate([
7271
7547
  ], SpeechOSWidget.prototype, "editHelpModalOpen", void 0);
7272
7548
  __decorate([
7273
7549
  r()
7274
- ], SpeechOSWidget.prototype, "commandFeedback", void 0);
7550
+ ], SpeechOSWidget.prototype, "actionFeedback", void 0);
7551
+ __decorate([
7552
+ r()
7553
+ ], SpeechOSWidget.prototype, "showNoAudioWarning", void 0);
7275
7554
  SpeechOSWidget = SpeechOSWidget_1 = __decorate([
7276
7555
  t$1("speechos-widget")
7277
7556
  ], SpeechOSWidget);