@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.cjs CHANGED
@@ -1760,7 +1760,8 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
1760
1760
  this.activeAction = null;
1761
1761
  this.editPreviewText = "";
1762
1762
  this.errorMessage = null;
1763
- this.commandFeedback = null;
1763
+ this.actionFeedback = null;
1764
+ this.showNoAudioWarning = false;
1764
1765
  }
1765
1766
  static { this.styles = [
1766
1767
  themeStyles,
@@ -2350,8 +2351,9 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2350
2351
  background-position: center;
2351
2352
  }
2352
2353
 
2353
- /* Command feedback badge - no match state (neutral gray) */
2354
- .status-label.command-none {
2354
+ /* Command/edit feedback badge - no match/empty state (neutral gray) */
2355
+ .status-label.command-none,
2356
+ .status-label.edit-empty {
2355
2357
  background: #4b5563;
2356
2358
  box-shadow: 0 4px 12px rgba(75, 85, 99, 0.3);
2357
2359
  animation: command-feedback-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)
@@ -2508,6 +2510,60 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2508
2510
  border-color: rgba(255, 255, 255, 0.5);
2509
2511
  }
2510
2512
 
2513
+ /* No audio warning banner */
2514
+ .no-audio-warning {
2515
+ position: absolute;
2516
+ bottom: 120px; /* Above button and waveform visualizer */
2517
+ left: 50%;
2518
+ transform: translateX(-50%) translateY(8px);
2519
+ display: flex;
2520
+ align-items: center;
2521
+ gap: 8px;
2522
+ padding: 10px 14px;
2523
+ border-radius: 12px;
2524
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
2525
+ box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
2526
+ transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
2527
+ pointer-events: none;
2528
+ opacity: 0;
2529
+ white-space: nowrap;
2530
+ }
2531
+
2532
+ .no-audio-warning.visible {
2533
+ opacity: 1;
2534
+ transform: translateX(-50%) translateY(0);
2535
+ pointer-events: auto;
2536
+ }
2537
+
2538
+ .no-audio-warning .warning-icon {
2539
+ flex-shrink: 0;
2540
+ color: white;
2541
+ }
2542
+
2543
+ .no-audio-warning .warning-text {
2544
+ font-size: 13px;
2545
+ font-weight: 500;
2546
+ color: white;
2547
+ }
2548
+
2549
+ .no-audio-warning .settings-link {
2550
+ background: rgba(255, 255, 255, 0.2);
2551
+ border: 1px solid rgba(255, 255, 255, 0.3);
2552
+ border-radius: 6px;
2553
+ padding: 4px 10px;
2554
+ font-size: 12px;
2555
+ font-weight: 600;
2556
+ color: white;
2557
+ cursor: pointer;
2558
+ transition: all 0.15s;
2559
+ white-space: nowrap;
2560
+ }
2561
+
2562
+ .no-audio-warning .settings-link:hover {
2563
+ background: rgba(255, 255, 255, 0.3);
2564
+ border-color: rgba(255, 255, 255, 0.5);
2565
+ }
2566
+
2511
2567
  /* Mobile styles - 30% larger */
2512
2568
  @media (max-width: 768px) and (hover: none) {
2513
2569
  .mic-button {
@@ -2637,6 +2693,21 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2637
2693
  padding: 8px 14px;
2638
2694
  font-size: 14px;
2639
2695
  }
2696
+
2697
+ .no-audio-warning {
2698
+ padding: 12px 16px;
2699
+ gap: 10px;
2700
+ bottom: 145px; /* Above button and waveform on mobile */
2701
+ }
2702
+
2703
+ .no-audio-warning .warning-text {
2704
+ font-size: 15px;
2705
+ }
2706
+
2707
+ .no-audio-warning .settings-link {
2708
+ padding: 6px 12px;
2709
+ font-size: 14px;
2710
+ }
2640
2711
  }
2641
2712
  `,
2642
2713
  ]; }
@@ -2694,6 +2765,14 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2694
2765
  composed: true,
2695
2766
  }));
2696
2767
  }
2768
+ handleOpenSettings(e) {
2769
+ e.stopPropagation();
2770
+ e.preventDefault();
2771
+ this.dispatchEvent(new CustomEvent("open-settings", {
2772
+ bubbles: true,
2773
+ composed: true,
2774
+ }));
2775
+ }
2697
2776
  getButtonClass() {
2698
2777
  const classes = ["mic-button"];
2699
2778
  if (this.expanded && this.recordingState === "idle") {
@@ -2778,13 +2857,16 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2778
2857
  }
2779
2858
  return this.recordingState;
2780
2859
  }
2781
- getCommandFeedbackLabel() {
2782
- if (this.commandFeedback === "success") {
2860
+ getActionFeedbackLabel() {
2861
+ if (this.actionFeedback === "command-success") {
2783
2862
  return "Got it!";
2784
2863
  }
2785
- if (this.commandFeedback === "none") {
2864
+ if (this.actionFeedback === "command-none") {
2786
2865
  return "No command matched";
2787
2866
  }
2867
+ if (this.actionFeedback === "edit-empty") {
2868
+ return "Couldn't understand edit";
2869
+ }
2788
2870
  return "";
2789
2871
  }
2790
2872
  render() {
@@ -2794,9 +2876,9 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2794
2876
  const showSiriEdit = this.recordingState === "processing" && this.activeAction === "edit";
2795
2877
  const statusLabel = this.getStatusLabel();
2796
2878
  const showVisualizer = this.shouldShowVisualizer();
2797
- // Show status label during recording (either visualizer or edit text) OR command feedback
2798
- const showCommandFeedback = this.recordingState === "idle" && this.commandFeedback !== null;
2799
- const showStatus = this.recordingState === "recording" || showCommandFeedback;
2879
+ // Show status label during recording (either visualizer or edit text) OR action feedback
2880
+ const showActionFeedback = this.recordingState === "idle" && this.actionFeedback !== null;
2881
+ const showStatus = this.recordingState === "recording" || showActionFeedback;
2800
2882
  const showCancel = this.recordingState === "connecting" ||
2801
2883
  this.recordingState === "recording" ||
2802
2884
  this.recordingState === "processing";
@@ -2835,6 +2917,35 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2835
2917
  `
2836
2918
  : ""}
2837
2919
 
2920
+ <div
2921
+ class="no-audio-warning ${this.showNoAudioWarning &&
2922
+ this.recordingState === "recording"
2923
+ ? "visible"
2924
+ : ""}"
2925
+ >
2926
+ <svg
2927
+ class="warning-icon"
2928
+ width="16"
2929
+ height="16"
2930
+ viewBox="0 0 24 24"
2931
+ fill="none"
2932
+ stroke="currentColor"
2933
+ stroke-width="2"
2934
+ stroke-linecap="round"
2935
+ stroke-linejoin="round"
2936
+ >
2937
+ <path
2938
+ 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"
2939
+ />
2940
+ <line x1="12" y1="9" x2="12" y2="13" />
2941
+ <line x1="12" y1="17" x2="12.01" y2="17" />
2942
+ </svg>
2943
+ <span class="warning-text">We're not hearing anything</span>
2944
+ <button class="settings-link" @click="${this.handleOpenSettings}">
2945
+ Check Settings
2946
+ </button>
2947
+ </div>
2948
+
2838
2949
  <button
2839
2950
  class="${this.getButtonClass()}"
2840
2951
  @click="${this.handleClick}"
@@ -2847,14 +2958,14 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2847
2958
  </button>
2848
2959
 
2849
2960
  <span
2850
- class="status-label ${showStatus ? "visible" : ""} ${showCommandFeedback
2851
- ? `command-${this.commandFeedback}`
2961
+ class="status-label ${showStatus ? "visible" : ""} ${showActionFeedback
2962
+ ? this.actionFeedback
2852
2963
  : showVisualizer
2853
2964
  ? "visualizer"
2854
2965
  : this.getStatusClass()}"
2855
2966
  >
2856
- ${showCommandFeedback
2857
- ? this.getCommandFeedbackLabel()
2967
+ ${showActionFeedback
2968
+ ? this.getActionFeedbackLabel()
2858
2969
  : showVisualizer
2859
2970
  ? b `<speechos-audio-visualizer
2860
2971
  ?active="${showVisualizer}"
@@ -2902,7 +3013,10 @@ __decorate([
2902
3013
  ], SpeechOSMicButton.prototype, "errorMessage", void 0);
2903
3014
  __decorate([
2904
3015
  n({ type: String })
2905
- ], SpeechOSMicButton.prototype, "commandFeedback", void 0);
3016
+ ], SpeechOSMicButton.prototype, "actionFeedback", void 0);
3017
+ __decorate([
3018
+ n({ type: Boolean })
3019
+ ], SpeechOSMicButton.prototype, "showNoAudioWarning", void 0);
2906
3020
  SpeechOSMicButton = __decorate([
2907
3021
  t$1("speechos-mic-button")
2908
3022
  ], SpeechOSMicButton);
@@ -6276,15 +6390,27 @@ var SpeechOSWidget_1;
6276
6390
  * duration so users can see the visual feedback before transitioning to recording.
6277
6391
  */
6278
6392
  const MIN_CONNECTING_ANIMATION_MS = 200;
6393
+ /**
6394
+ * Time to wait for a transcription event before showing the "no audio" warning (in milliseconds).
6395
+ * If no transcription:interim event is received within this time during recording,
6396
+ * it indicates the server isn't receiving/processing audio.
6397
+ */
6398
+ const NO_AUDIO_WARNING_TIMEOUT_MS = 5000;
6399
+ /**
6400
+ * Number of consecutive actions with empty results before showing warning on next action.
6401
+ */
6402
+ const CONSECUTIVE_NO_AUDIO_THRESHOLD = 2;
6279
6403
  let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6280
6404
  constructor() {
6281
6405
  super(...arguments);
6282
6406
  this.widgetState = core.state.getState();
6283
6407
  this.settingsOpen = false;
6408
+ this.settingsOpenFromWarning = false;
6284
6409
  this.dictationModalOpen = false;
6285
6410
  this.dictationModalText = "";
6286
6411
  this.editHelpModalOpen = false;
6287
- this.commandFeedback = null;
6412
+ this.actionFeedback = null;
6413
+ this.showNoAudioWarning = false;
6288
6414
  this.dictationTargetElement = null;
6289
6415
  this.editTargetElement = null;
6290
6416
  this.dictationCursorStart = null;
@@ -6296,7 +6422,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6296
6422
  this.modalElement = null;
6297
6423
  this.dictationModalElement = null;
6298
6424
  this.editHelpModalElement = null;
6299
- this.commandFeedbackTimeout = null;
6425
+ this.actionFeedbackTimeout = null;
6300
6426
  this.customPosition = null;
6301
6427
  this.isDragging = false;
6302
6428
  this.dragStartPos = null;
@@ -6306,6 +6432,11 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6306
6432
  this.suppressNextClick = false;
6307
6433
  this.boundViewportResizeHandler = null;
6308
6434
  this.boundScrollHandler = null;
6435
+ // No-audio warning state tracking
6436
+ this.consecutiveNoAudioActions = 0;
6437
+ this.transcriptionReceived = false;
6438
+ this.noAudioWarningTimeout = null;
6439
+ this.transcriptionInterimUnsubscribe = null;
6309
6440
  }
6310
6441
  static { SpeechOSWidget_1 = this; }
6311
6442
  static { this.styles = [
@@ -6388,6 +6519,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6388
6519
  this.modalElement = document.createElement("speechos-settings-modal");
6389
6520
  this.modalElement.addEventListener("modal-close", () => {
6390
6521
  this.settingsOpen = false;
6522
+ this.settingsOpenFromWarning = false;
6391
6523
  });
6392
6524
  document.body.appendChild(this.modalElement);
6393
6525
  // Mount dictation output modal
@@ -6403,7 +6535,17 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6403
6535
  });
6404
6536
  document.body.appendChild(this.editHelpModalElement);
6405
6537
  this.stateUnsubscribe = core.state.subscribe((newState) => {
6406
- if (!newState.isVisible || !newState.isExpanded) {
6538
+ if (!newState.isVisible) {
6539
+ if (core.getConfig().debug && this.settingsOpen) {
6540
+ console.log("[SpeechOS] Closing settings modal: widget hidden");
6541
+ }
6542
+ this.settingsOpen = false;
6543
+ this.settingsOpenFromWarning = false;
6544
+ }
6545
+ else if (!newState.isExpanded && !this.settingsOpenFromWarning) {
6546
+ if (core.getConfig().debug && this.settingsOpen) {
6547
+ console.log("[SpeechOS] Closing settings modal: widget collapsed");
6548
+ }
6407
6549
  this.settingsOpen = false;
6408
6550
  }
6409
6551
  // Clear custom position when focused element changes (re-anchor to new element)
@@ -6449,9 +6591,9 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6449
6591
  this.editHelpModalElement.remove();
6450
6592
  this.editHelpModalElement = null;
6451
6593
  }
6452
- if (this.commandFeedbackTimeout) {
6453
- clearTimeout(this.commandFeedbackTimeout);
6454
- this.commandFeedbackTimeout = null;
6594
+ if (this.actionFeedbackTimeout) {
6595
+ clearTimeout(this.actionFeedbackTimeout);
6596
+ this.actionFeedbackTimeout = null;
6455
6597
  }
6456
6598
  if (this.stateUnsubscribe) {
6457
6599
  this.stateUnsubscribe();
@@ -6479,6 +6621,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6479
6621
  window.removeEventListener("scroll", this.boundScrollHandler);
6480
6622
  this.boundScrollHandler = null;
6481
6623
  }
6624
+ this.cleanupNoAudioWarningTracking();
6482
6625
  }
6483
6626
  updated(changedProperties) {
6484
6627
  if (changedProperties.has("settingsOpen") && this.modalElement) {
@@ -6656,7 +6799,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6656
6799
  }
6657
6800
  if (this.widgetState.recordingState === "idle") {
6658
6801
  // Clear command feedback on any mic click
6659
- this.clearCommandFeedback();
6802
+ this.clearActionFeedback();
6660
6803
  // If we're expanding, prefetch the token to reduce latency when user selects an action
6661
6804
  if (!this.widgetState.isExpanded) {
6662
6805
  // Fire and forget - we don't need to wait for this (LiveKit only)
@@ -6677,6 +6820,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6677
6820
  }
6678
6821
  }
6679
6822
  async handleStopRecording() {
6823
+ // Clean up no-audio warning tracking
6824
+ this.cleanupNoAudioWarningTracking();
6680
6825
  if (this.widgetState.activeAction === "edit") {
6681
6826
  await this.handleStopEdit();
6682
6827
  }
@@ -6688,6 +6833,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6688
6833
  const backend = core.getBackend();
6689
6834
  try {
6690
6835
  const transcription = await this.withMinDisplayTime(backend.stopVoiceSession(), 300);
6836
+ // Track result for consecutive failure detection
6837
+ this.trackActionResult(!!transcription);
6691
6838
  if (transcription) {
6692
6839
  // Check if we have a target element to insert into
6693
6840
  if (this.dictationTargetElement) {
@@ -6707,6 +6854,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6707
6854
  backend.startAutoRefresh?.();
6708
6855
  }
6709
6856
  catch (error) {
6857
+ // Track as failed result
6858
+ this.trackActionResult(false);
6710
6859
  const errorMessage = error instanceof Error ? error.message : "Failed to transcribe audio";
6711
6860
  if (errorMessage !== "Disconnected") {
6712
6861
  core.state.setError(errorMessage);
@@ -6716,6 +6865,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6716
6865
  }
6717
6866
  }
6718
6867
  async handleCancelOperation() {
6868
+ // Clean up no-audio warning tracking
6869
+ this.cleanupNoAudioWarningTracking();
6719
6870
  await core.getBackend().disconnect();
6720
6871
  if (this.widgetState.recordingState === "error") {
6721
6872
  core.state.clearError();
@@ -6745,7 +6896,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6745
6896
  }
6746
6897
  }
6747
6898
  handleCloseWidget() {
6748
- this.clearCommandFeedback();
6899
+ this.clearActionFeedback();
6749
6900
  core.getBackend().stopAutoRefresh?.();
6750
6901
  core.state.hide();
6751
6902
  }
@@ -6874,7 +7025,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6874
7025
  handleActionSelect(event) {
6875
7026
  const { action } = event.detail;
6876
7027
  // Clear any existing command feedback when a new action is selected
6877
- this.clearCommandFeedback();
7028
+ this.clearActionFeedback();
6878
7029
  core.state.setActiveAction(action);
6879
7030
  if (action === "dictate") {
6880
7031
  this.startDictation();
@@ -6937,10 +7088,12 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6937
7088
  if (remainingDelay > 0) {
6938
7089
  setTimeout(() => {
6939
7090
  core.state.setRecordingState("recording");
7091
+ this.startNoAudioWarningTracking();
6940
7092
  }, remainingDelay);
6941
7093
  }
6942
7094
  else {
6943
7095
  core.state.setRecordingState("recording");
7096
+ this.startNoAudioWarningTracking();
6944
7097
  }
6945
7098
  },
6946
7099
  });
@@ -6948,7 +7101,10 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6948
7101
  catch (error) {
6949
7102
  const errorMessage = error instanceof Error ? error.message : "Connection failed";
6950
7103
  if (errorMessage !== "Disconnected") {
6951
- core.state.setError(`Failed to connect: ${errorMessage}`);
7104
+ // Only set error if not already in error state (error event may have already set it)
7105
+ if (this.widgetState.recordingState !== "error") {
7106
+ core.state.setError(`Failed to connect: ${errorMessage}`);
7107
+ }
6952
7108
  await backend.disconnect();
6953
7109
  }
6954
7110
  }
@@ -7003,10 +7159,12 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7003
7159
  if (remainingDelay > 0) {
7004
7160
  setTimeout(() => {
7005
7161
  core.state.setRecordingState("recording");
7162
+ this.startNoAudioWarningTracking();
7006
7163
  }, remainingDelay);
7007
7164
  }
7008
7165
  else {
7009
7166
  core.state.setRecordingState("recording");
7167
+ this.startNoAudioWarningTracking();
7010
7168
  }
7011
7169
  },
7012
7170
  });
@@ -7014,7 +7172,10 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7014
7172
  catch (error) {
7015
7173
  const errorMessage = error instanceof Error ? error.message : "Connection failed";
7016
7174
  if (errorMessage !== "Disconnected") {
7017
- core.state.setError(`Failed to connect: ${errorMessage}`);
7175
+ // Only set error if not already in error state (error event may have already set it)
7176
+ if (this.widgetState.recordingState !== "error") {
7177
+ core.state.setError(`Failed to connect: ${errorMessage}`);
7178
+ }
7018
7179
  await backend.disconnect();
7019
7180
  }
7020
7181
  }
@@ -7025,12 +7186,30 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7025
7186
  const backend = core.getBackend();
7026
7187
  try {
7027
7188
  const editedText = await this.withMinDisplayTime(backend.requestEditText(originalContent), 300);
7189
+ // Check if server returned no change (couldn't understand edit)
7190
+ const noChange = editedText.trim() === originalContent.trim();
7191
+ if (noChange) {
7192
+ this.trackActionResult(false);
7193
+ this.showActionFeedback("edit-empty");
7194
+ core.state.completeRecording();
7195
+ this.editTargetElement = null;
7196
+ this.editSelectionStart = null;
7197
+ this.editSelectionEnd = null;
7198
+ this.editSelectedText = "";
7199
+ backend.disconnect().catch(() => { });
7200
+ backend.startAutoRefresh?.();
7201
+ return;
7202
+ }
7203
+ // Track result - got a meaningful change
7204
+ this.trackActionResult(true);
7028
7205
  this.applyEdit(editedText);
7029
7206
  backend.disconnect().catch(() => { });
7030
7207
  // Start auto-refresh to keep token fresh for subsequent commands (LiveKit only)
7031
7208
  backend.startAutoRefresh?.();
7032
7209
  }
7033
7210
  catch (error) {
7211
+ // Track as failed result
7212
+ this.trackActionResult(false);
7034
7213
  const errorMessage = error instanceof Error ? error.message : "Failed to apply edit";
7035
7214
  if (errorMessage !== "Disconnected") {
7036
7215
  core.state.setError(errorMessage);
@@ -7056,10 +7235,12 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7056
7235
  if (remainingDelay > 0) {
7057
7236
  setTimeout(() => {
7058
7237
  core.state.setRecordingState("recording");
7238
+ this.startNoAudioWarningTracking();
7059
7239
  }, remainingDelay);
7060
7240
  }
7061
7241
  else {
7062
7242
  core.state.setRecordingState("recording");
7243
+ this.startNoAudioWarningTracking();
7063
7244
  }
7064
7245
  },
7065
7246
  });
@@ -7067,7 +7248,10 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7067
7248
  catch (error) {
7068
7249
  const errorMessage = error instanceof Error ? error.message : "Connection failed";
7069
7250
  if (errorMessage !== "Disconnected") {
7070
- core.state.setError(`Failed to connect: ${errorMessage}`);
7251
+ // Only set error if not already in error state (error event may have already set it)
7252
+ if (this.widgetState.recordingState !== "error") {
7253
+ core.state.setError(`Failed to connect: ${errorMessage}`);
7254
+ }
7071
7255
  await backend.disconnect();
7072
7256
  }
7073
7257
  }
@@ -7079,6 +7263,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7079
7263
  const backend = core.getBackend();
7080
7264
  try {
7081
7265
  const result = await this.withMinDisplayTime(backend.requestCommand(commands), 300);
7266
+ // Track result - null result means no command matched (possibly no audio)
7267
+ this.trackActionResult(result !== null);
7082
7268
  // Get input text from the backend if available
7083
7269
  const inputText = backend.getLastInputText?.();
7084
7270
  // Save to transcript store
@@ -7096,12 +7282,14 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7096
7282
  // Keep widget visible but collapsed (just mic button, no action bubbles)
7097
7283
  core.state.setState({ isExpanded: false });
7098
7284
  // Show command feedback
7099
- this.showCommandFeedback(result ? "success" : "none");
7285
+ this.showActionFeedback(result ? "command-success" : "command-none");
7100
7286
  backend.disconnect().catch(() => { });
7101
7287
  // Start auto-refresh to keep token fresh for subsequent commands (LiveKit only)
7102
7288
  backend.startAutoRefresh?.();
7103
7289
  }
7104
7290
  catch (error) {
7291
+ // Track as failed result
7292
+ this.trackActionResult(false);
7105
7293
  const errorMessage = error instanceof Error ? error.message : "Failed to process command";
7106
7294
  if (errorMessage !== "Disconnected") {
7107
7295
  core.state.setError(errorMessage);
@@ -7109,24 +7297,110 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7109
7297
  }
7110
7298
  }
7111
7299
  }
7112
- showCommandFeedback(feedback) {
7113
- this.commandFeedback = feedback;
7300
+ showActionFeedback(feedback) {
7301
+ this.actionFeedback = feedback;
7114
7302
  // Clear any existing timeout
7115
- if (this.commandFeedbackTimeout) {
7116
- clearTimeout(this.commandFeedbackTimeout);
7303
+ if (this.actionFeedbackTimeout) {
7304
+ clearTimeout(this.actionFeedbackTimeout);
7117
7305
  }
7118
7306
  // Auto-dismiss after 4 seconds
7119
- this.commandFeedbackTimeout = window.setTimeout(() => {
7120
- this.commandFeedback = null;
7121
- this.commandFeedbackTimeout = null;
7307
+ this.actionFeedbackTimeout = window.setTimeout(() => {
7308
+ this.actionFeedback = null;
7309
+ this.actionFeedbackTimeout = null;
7122
7310
  }, 4000);
7123
7311
  }
7124
- clearCommandFeedback() {
7125
- if (this.commandFeedbackTimeout) {
7126
- clearTimeout(this.commandFeedbackTimeout);
7127
- this.commandFeedbackTimeout = null;
7312
+ clearActionFeedback() {
7313
+ if (this.actionFeedbackTimeout) {
7314
+ clearTimeout(this.actionFeedbackTimeout);
7315
+ this.actionFeedbackTimeout = null;
7128
7316
  }
7129
- this.commandFeedback = null;
7317
+ this.actionFeedback = null;
7318
+ }
7319
+ /**
7320
+ * Start tracking for no-audio warning when recording begins.
7321
+ */
7322
+ startNoAudioWarningTracking() {
7323
+ this.transcriptionReceived = false;
7324
+ this.showNoAudioWarning = false;
7325
+ // If we had consecutive failures, show warning immediately
7326
+ if (this.consecutiveNoAudioActions >= CONSECUTIVE_NO_AUDIO_THRESHOLD) {
7327
+ this.showNoAudioWarning = true;
7328
+ }
7329
+ // Start timeout - if no transcription within 5s, show warning
7330
+ this.noAudioWarningTimeout = window.setTimeout(() => {
7331
+ if (!this.transcriptionReceived &&
7332
+ this.widgetState.recordingState === "recording") {
7333
+ this.showNoAudioWarning = true;
7334
+ }
7335
+ }, NO_AUDIO_WARNING_TIMEOUT_MS);
7336
+ // Subscribe to transcription:interim events
7337
+ this.transcriptionInterimUnsubscribe = core.events.on("transcription:interim", () => {
7338
+ this.transcriptionReceived = true;
7339
+ if (this.showNoAudioWarning) {
7340
+ this.showNoAudioWarning = false;
7341
+ }
7342
+ });
7343
+ }
7344
+ /**
7345
+ * Clean up no-audio warning tracking when recording stops.
7346
+ */
7347
+ cleanupNoAudioWarningTracking() {
7348
+ if (this.noAudioWarningTimeout !== null) {
7349
+ clearTimeout(this.noAudioWarningTimeout);
7350
+ this.noAudioWarningTimeout = null;
7351
+ }
7352
+ if (this.transcriptionInterimUnsubscribe) {
7353
+ this.transcriptionInterimUnsubscribe();
7354
+ this.transcriptionInterimUnsubscribe = null;
7355
+ }
7356
+ this.showNoAudioWarning = false;
7357
+ }
7358
+ /**
7359
+ * Track the result of an action for consecutive failure detection.
7360
+ */
7361
+ trackActionResult(hasContent) {
7362
+ if (hasContent) {
7363
+ this.consecutiveNoAudioActions = 0;
7364
+ }
7365
+ else {
7366
+ this.consecutiveNoAudioActions++;
7367
+ }
7368
+ }
7369
+ /**
7370
+ * Handle opening settings from the no-audio warning.
7371
+ * Stops the current dictation session immediately, then opens settings.
7372
+ */
7373
+ async handleOpenSettingsFromWarning() {
7374
+ if (core.getConfig().debug) {
7375
+ console.log("[SpeechOS] No-audio settings link clicked");
7376
+ }
7377
+ // Clean up no-audio warning tracking first
7378
+ this.cleanupNoAudioWarningTracking();
7379
+ // Keep settings open even if widget collapses
7380
+ this.settingsOpenFromWarning = true;
7381
+ // Stop audio capture and disconnect immediately (don't wait for transcription)
7382
+ // Kick this off before opening settings so audio stops fast, but don't block UI.
7383
+ const disconnectPromise = core.getBackend().disconnect().catch((error) => {
7384
+ if (core.getConfig().debug) {
7385
+ console.log("[SpeechOS] Disconnect failed while opening settings", error);
7386
+ }
7387
+ });
7388
+ // Update UI state to idle
7389
+ core.state.cancelRecording();
7390
+ // Clear target elements
7391
+ this.dictationTargetElement = null;
7392
+ this.editTargetElement = null;
7393
+ this.dictationCursorStart = null;
7394
+ this.dictationCursorEnd = null;
7395
+ this.editSelectionStart = null;
7396
+ this.editSelectionEnd = null;
7397
+ this.editSelectedText = "";
7398
+ // Open settings modal
7399
+ this.settingsOpen = true;
7400
+ if (core.getConfig().debug) {
7401
+ console.log("[SpeechOS] Settings modal opened from no-audio warning");
7402
+ }
7403
+ await disconnectPromise;
7130
7404
  }
7131
7405
  supportsSelection(element) {
7132
7406
  if (element.tagName.toLowerCase() === "textarea") {
@@ -7245,12 +7519,14 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7245
7519
  activeAction="${this.widgetState.activeAction || ""}"
7246
7520
  editPreviewText="${this.editSelectedText}"
7247
7521
  errorMessage="${this.widgetState.errorMessage || ""}"
7248
- .commandFeedback="${this.commandFeedback}"
7522
+ .actionFeedback="${this.actionFeedback}"
7523
+ ?showNoAudioWarning="${this.showNoAudioWarning}"
7249
7524
  @mic-click="${this.handleMicClick}"
7250
7525
  @stop-recording="${this.handleStopRecording}"
7251
7526
  @cancel-operation="${this.handleCancelOperation}"
7252
7527
  @retry-connection="${this.handleRetryConnection}"
7253
7528
  @close-widget="${this.handleCloseWidget}"
7529
+ @open-settings="${this.handleOpenSettingsFromWarning}"
7254
7530
  ></speechos-mic-button>
7255
7531
  </div>
7256
7532
  </div>
@@ -7274,7 +7550,10 @@ __decorate([
7274
7550
  ], SpeechOSWidget.prototype, "editHelpModalOpen", void 0);
7275
7551
  __decorate([
7276
7552
  r()
7277
- ], SpeechOSWidget.prototype, "commandFeedback", void 0);
7553
+ ], SpeechOSWidget.prototype, "actionFeedback", void 0);
7554
+ __decorate([
7555
+ r()
7556
+ ], SpeechOSWidget.prototype, "showNoAudioWarning", void 0);
7278
7557
  SpeechOSWidget = SpeechOSWidget_1 = __decorate([
7279
7558
  t$1("speechos-widget")
7280
7559
  ], SpeechOSWidget);