@speechos/client 0.2.4 → 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.
@@ -28518,6 +28518,10 @@
28518
28518
  }
28519
28519
  handleIntermediateTranscription(message) {
28520
28520
  const config = getConfig();
28521
+ events.emit("transcription:interim", {
28522
+ transcript: message.transcript,
28523
+ isFinal: message.is_final
28524
+ });
28521
28525
  if (config.debug) console.log("[SpeechOS] Intermediate transcription:", message.transcript, "final:", message.is_final);
28522
28526
  }
28523
28527
  handleFinalTranscript(message) {
@@ -30508,7 +30512,8 @@
30508
30512
  this.activeAction = null;
30509
30513
  this.editPreviewText = "";
30510
30514
  this.errorMessage = null;
30511
- this.commandFeedback = null;
30515
+ this.actionFeedback = null;
30516
+ this.showNoAudioWarning = false;
30512
30517
  }
30513
30518
  static { this.styles = [
30514
30519
  themeStyles,
@@ -31098,8 +31103,9 @@
31098
31103
  background-position: center;
31099
31104
  }
31100
31105
 
31101
- /* Command feedback badge - no match state (neutral gray) */
31102
- .status-label.command-none {
31106
+ /* Command/edit feedback badge - no match/empty state (neutral gray) */
31107
+ .status-label.command-none,
31108
+ .status-label.edit-empty {
31103
31109
  background: #4b5563;
31104
31110
  box-shadow: 0 4px 12px rgba(75, 85, 99, 0.3);
31105
31111
  animation: command-feedback-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)
@@ -31256,6 +31262,60 @@
31256
31262
  border-color: rgba(255, 255, 255, 0.5);
31257
31263
  }
31258
31264
 
31265
+ /* No audio warning banner */
31266
+ .no-audio-warning {
31267
+ position: absolute;
31268
+ bottom: 120px; /* Above button and waveform visualizer */
31269
+ left: 50%;
31270
+ transform: translateX(-50%) translateY(8px);
31271
+ display: flex;
31272
+ align-items: center;
31273
+ gap: 8px;
31274
+ padding: 10px 14px;
31275
+ border-radius: 12px;
31276
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
31277
+ box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
31278
+ transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
31279
+ pointer-events: none;
31280
+ opacity: 0;
31281
+ white-space: nowrap;
31282
+ }
31283
+
31284
+ .no-audio-warning.visible {
31285
+ opacity: 1;
31286
+ transform: translateX(-50%) translateY(0);
31287
+ pointer-events: auto;
31288
+ }
31289
+
31290
+ .no-audio-warning .warning-icon {
31291
+ flex-shrink: 0;
31292
+ color: white;
31293
+ }
31294
+
31295
+ .no-audio-warning .warning-text {
31296
+ font-size: 13px;
31297
+ font-weight: 500;
31298
+ color: white;
31299
+ }
31300
+
31301
+ .no-audio-warning .settings-link {
31302
+ background: rgba(255, 255, 255, 0.2);
31303
+ border: 1px solid rgba(255, 255, 255, 0.3);
31304
+ border-radius: 6px;
31305
+ padding: 4px 10px;
31306
+ font-size: 12px;
31307
+ font-weight: 600;
31308
+ color: white;
31309
+ cursor: pointer;
31310
+ transition: all 0.15s;
31311
+ white-space: nowrap;
31312
+ }
31313
+
31314
+ .no-audio-warning .settings-link:hover {
31315
+ background: rgba(255, 255, 255, 0.3);
31316
+ border-color: rgba(255, 255, 255, 0.5);
31317
+ }
31318
+
31259
31319
  /* Mobile styles - 30% larger */
31260
31320
  @media (max-width: 768px) and (hover: none) {
31261
31321
  .mic-button {
@@ -31385,6 +31445,21 @@
31385
31445
  padding: 8px 14px;
31386
31446
  font-size: 14px;
31387
31447
  }
31448
+
31449
+ .no-audio-warning {
31450
+ padding: 12px 16px;
31451
+ gap: 10px;
31452
+ bottom: 145px; /* Above button and waveform on mobile */
31453
+ }
31454
+
31455
+ .no-audio-warning .warning-text {
31456
+ font-size: 15px;
31457
+ }
31458
+
31459
+ .no-audio-warning .settings-link {
31460
+ padding: 6px 12px;
31461
+ font-size: 14px;
31462
+ }
31388
31463
  }
31389
31464
  `,
31390
31465
  ]; }
@@ -31442,6 +31517,14 @@
31442
31517
  composed: true,
31443
31518
  }));
31444
31519
  }
31520
+ handleOpenSettings(e) {
31521
+ e.stopPropagation();
31522
+ e.preventDefault();
31523
+ this.dispatchEvent(new CustomEvent("open-settings", {
31524
+ bubbles: true,
31525
+ composed: true,
31526
+ }));
31527
+ }
31445
31528
  getButtonClass() {
31446
31529
  const classes = ["mic-button"];
31447
31530
  if (this.expanded && this.recordingState === "idle") {
@@ -31526,13 +31609,16 @@
31526
31609
  }
31527
31610
  return this.recordingState;
31528
31611
  }
31529
- getCommandFeedbackLabel() {
31530
- if (this.commandFeedback === "success") {
31612
+ getActionFeedbackLabel() {
31613
+ if (this.actionFeedback === "command-success") {
31531
31614
  return "Got it!";
31532
31615
  }
31533
- if (this.commandFeedback === "none") {
31616
+ if (this.actionFeedback === "command-none") {
31534
31617
  return "No command matched";
31535
31618
  }
31619
+ if (this.actionFeedback === "edit-empty") {
31620
+ return "Couldn't understand edit";
31621
+ }
31536
31622
  return "";
31537
31623
  }
31538
31624
  render() {
@@ -31542,9 +31628,9 @@
31542
31628
  const showSiriEdit = this.recordingState === "processing" && this.activeAction === "edit";
31543
31629
  const statusLabel = this.getStatusLabel();
31544
31630
  const showVisualizer = this.shouldShowVisualizer();
31545
- // Show status label during recording (either visualizer or edit text) OR command feedback
31546
- const showCommandFeedback = this.recordingState === "idle" && this.commandFeedback !== null;
31547
- const showStatus = this.recordingState === "recording" || showCommandFeedback;
31631
+ // Show status label during recording (either visualizer or edit text) OR action feedback
31632
+ const showActionFeedback = this.recordingState === "idle" && this.actionFeedback !== null;
31633
+ const showStatus = this.recordingState === "recording" || showActionFeedback;
31548
31634
  const showCancel = this.recordingState === "connecting" ||
31549
31635
  this.recordingState === "recording" ||
31550
31636
  this.recordingState === "processing";
@@ -31583,6 +31669,35 @@
31583
31669
  `
31584
31670
  : ""}
31585
31671
 
31672
+ <div
31673
+ class="no-audio-warning ${this.showNoAudioWarning &&
31674
+ this.recordingState === "recording"
31675
+ ? "visible"
31676
+ : ""}"
31677
+ >
31678
+ <svg
31679
+ class="warning-icon"
31680
+ width="16"
31681
+ height="16"
31682
+ viewBox="0 0 24 24"
31683
+ fill="none"
31684
+ stroke="currentColor"
31685
+ stroke-width="2"
31686
+ stroke-linecap="round"
31687
+ stroke-linejoin="round"
31688
+ >
31689
+ <path
31690
+ 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"
31691
+ />
31692
+ <line x1="12" y1="9" x2="12" y2="13" />
31693
+ <line x1="12" y1="17" x2="12.01" y2="17" />
31694
+ </svg>
31695
+ <span class="warning-text">We're not hearing anything</span>
31696
+ <button class="settings-link" @click="${this.handleOpenSettings}">
31697
+ Check Settings
31698
+ </button>
31699
+ </div>
31700
+
31586
31701
  <button
31587
31702
  class="${this.getButtonClass()}"
31588
31703
  @click="${this.handleClick}"
@@ -31595,14 +31710,14 @@
31595
31710
  </button>
31596
31711
 
31597
31712
  <span
31598
- class="status-label ${showStatus ? "visible" : ""} ${showCommandFeedback
31599
- ? `command-${this.commandFeedback}`
31713
+ class="status-label ${showStatus ? "visible" : ""} ${showActionFeedback
31714
+ ? this.actionFeedback
31600
31715
  : showVisualizer
31601
31716
  ? "visualizer"
31602
31717
  : this.getStatusClass()}"
31603
31718
  >
31604
- ${showCommandFeedback
31605
- ? this.getCommandFeedbackLabel()
31719
+ ${showActionFeedback
31720
+ ? this.getActionFeedbackLabel()
31606
31721
  : showVisualizer
31607
31722
  ? b `<speechos-audio-visualizer
31608
31723
  ?active="${showVisualizer}"
@@ -31650,7 +31765,10 @@
31650
31765
  ], SpeechOSMicButton.prototype, "errorMessage", void 0);
31651
31766
  __decorate([
31652
31767
  n({ type: String })
31653
- ], SpeechOSMicButton.prototype, "commandFeedback", void 0);
31768
+ ], SpeechOSMicButton.prototype, "actionFeedback", void 0);
31769
+ __decorate([
31770
+ n({ type: Boolean })
31771
+ ], SpeechOSMicButton.prototype, "showNoAudioWarning", void 0);
31654
31772
  SpeechOSMicButton = __decorate([
31655
31773
  t$1("speechos-mic-button")
31656
31774
  ], SpeechOSMicButton);
@@ -35024,15 +35142,27 @@
35024
35142
  * duration so users can see the visual feedback before transitioning to recording.
35025
35143
  */
35026
35144
  const MIN_CONNECTING_ANIMATION_MS = 200;
35145
+ /**
35146
+ * Time to wait for a transcription event before showing the "no audio" warning (in milliseconds).
35147
+ * If no transcription:interim event is received within this time during recording,
35148
+ * it indicates the server isn't receiving/processing audio.
35149
+ */
35150
+ const NO_AUDIO_WARNING_TIMEOUT_MS = 5000;
35151
+ /**
35152
+ * Number of consecutive actions with empty results before showing warning on next action.
35153
+ */
35154
+ const CONSECUTIVE_NO_AUDIO_THRESHOLD = 2;
35027
35155
  let SpeechOSWidget = class SpeechOSWidget extends i$1 {
35028
35156
  constructor() {
35029
35157
  super(...arguments);
35030
35158
  this.widgetState = state.getState();
35031
35159
  this.settingsOpen = false;
35160
+ this.settingsOpenFromWarning = false;
35032
35161
  this.dictationModalOpen = false;
35033
35162
  this.dictationModalText = "";
35034
35163
  this.editHelpModalOpen = false;
35035
- this.commandFeedback = null;
35164
+ this.actionFeedback = null;
35165
+ this.showNoAudioWarning = false;
35036
35166
  this.dictationTargetElement = null;
35037
35167
  this.editTargetElement = null;
35038
35168
  this.dictationCursorStart = null;
@@ -35044,7 +35174,7 @@
35044
35174
  this.modalElement = null;
35045
35175
  this.dictationModalElement = null;
35046
35176
  this.editHelpModalElement = null;
35047
- this.commandFeedbackTimeout = null;
35177
+ this.actionFeedbackTimeout = null;
35048
35178
  this.customPosition = null;
35049
35179
  this.isDragging = false;
35050
35180
  this.dragStartPos = null;
@@ -35054,6 +35184,11 @@
35054
35184
  this.suppressNextClick = false;
35055
35185
  this.boundViewportResizeHandler = null;
35056
35186
  this.boundScrollHandler = null;
35187
+ // No-audio warning state tracking
35188
+ this.consecutiveNoAudioActions = 0;
35189
+ this.transcriptionReceived = false;
35190
+ this.noAudioWarningTimeout = null;
35191
+ this.transcriptionInterimUnsubscribe = null;
35057
35192
  }
35058
35193
  static { SpeechOSWidget_1 = this; }
35059
35194
  static { this.styles = [
@@ -35136,6 +35271,7 @@
35136
35271
  this.modalElement = document.createElement("speechos-settings-modal");
35137
35272
  this.modalElement.addEventListener("modal-close", () => {
35138
35273
  this.settingsOpen = false;
35274
+ this.settingsOpenFromWarning = false;
35139
35275
  });
35140
35276
  document.body.appendChild(this.modalElement);
35141
35277
  // Mount dictation output modal
@@ -35151,7 +35287,17 @@
35151
35287
  });
35152
35288
  document.body.appendChild(this.editHelpModalElement);
35153
35289
  this.stateUnsubscribe = state.subscribe((newState) => {
35154
- if (!newState.isVisible || !newState.isExpanded) {
35290
+ if (!newState.isVisible) {
35291
+ if (getConfig().debug && this.settingsOpen) {
35292
+ console.log("[SpeechOS] Closing settings modal: widget hidden");
35293
+ }
35294
+ this.settingsOpen = false;
35295
+ this.settingsOpenFromWarning = false;
35296
+ }
35297
+ else if (!newState.isExpanded && !this.settingsOpenFromWarning) {
35298
+ if (getConfig().debug && this.settingsOpen) {
35299
+ console.log("[SpeechOS] Closing settings modal: widget collapsed");
35300
+ }
35155
35301
  this.settingsOpen = false;
35156
35302
  }
35157
35303
  // Clear custom position when focused element changes (re-anchor to new element)
@@ -35197,9 +35343,9 @@
35197
35343
  this.editHelpModalElement.remove();
35198
35344
  this.editHelpModalElement = null;
35199
35345
  }
35200
- if (this.commandFeedbackTimeout) {
35201
- clearTimeout(this.commandFeedbackTimeout);
35202
- this.commandFeedbackTimeout = null;
35346
+ if (this.actionFeedbackTimeout) {
35347
+ clearTimeout(this.actionFeedbackTimeout);
35348
+ this.actionFeedbackTimeout = null;
35203
35349
  }
35204
35350
  if (this.stateUnsubscribe) {
35205
35351
  this.stateUnsubscribe();
@@ -35227,6 +35373,7 @@
35227
35373
  window.removeEventListener("scroll", this.boundScrollHandler);
35228
35374
  this.boundScrollHandler = null;
35229
35375
  }
35376
+ this.cleanupNoAudioWarningTracking();
35230
35377
  }
35231
35378
  updated(changedProperties) {
35232
35379
  if (changedProperties.has("settingsOpen") && this.modalElement) {
@@ -35403,7 +35550,7 @@
35403
35550
  }
35404
35551
  if (this.widgetState.recordingState === "idle") {
35405
35552
  // Clear command feedback on any mic click
35406
- this.clearCommandFeedback();
35553
+ this.clearActionFeedback();
35407
35554
  // If we're expanding, prefetch the token to reduce latency when user selects an action
35408
35555
  if (!this.widgetState.isExpanded) {
35409
35556
  // Fire and forget - we don't need to wait for this (LiveKit only)
@@ -35420,6 +35567,8 @@
35420
35567
  }
35421
35568
  }
35422
35569
  async handleStopRecording() {
35570
+ // Clean up no-audio warning tracking
35571
+ this.cleanupNoAudioWarningTracking();
35423
35572
  if (this.widgetState.activeAction === "edit") {
35424
35573
  await this.handleStopEdit();
35425
35574
  }
@@ -35431,6 +35580,8 @@
35431
35580
  const backend = getBackend();
35432
35581
  try {
35433
35582
  const transcription = await this.withMinDisplayTime(backend.stopVoiceSession(), 300);
35583
+ // Track result for consecutive failure detection
35584
+ this.trackActionResult(!!transcription);
35434
35585
  if (transcription) {
35435
35586
  // Check if we have a target element to insert into
35436
35587
  if (this.dictationTargetElement) {
@@ -35450,6 +35601,8 @@
35450
35601
  backend.startAutoRefresh?.();
35451
35602
  }
35452
35603
  catch (error) {
35604
+ // Track as failed result
35605
+ this.trackActionResult(false);
35453
35606
  const errorMessage = error instanceof Error ? error.message : "Failed to transcribe audio";
35454
35607
  if (errorMessage !== "Disconnected") {
35455
35608
  state.setError(errorMessage);
@@ -35459,6 +35612,8 @@
35459
35612
  }
35460
35613
  }
35461
35614
  async handleCancelOperation() {
35615
+ // Clean up no-audio warning tracking
35616
+ this.cleanupNoAudioWarningTracking();
35462
35617
  await getBackend().disconnect();
35463
35618
  if (this.widgetState.recordingState === "error") {
35464
35619
  state.clearError();
@@ -35488,7 +35643,7 @@
35488
35643
  }
35489
35644
  }
35490
35645
  handleCloseWidget() {
35491
- this.clearCommandFeedback();
35646
+ this.clearActionFeedback();
35492
35647
  state.hide();
35493
35648
  }
35494
35649
  handleSettingsClick() {
@@ -35616,7 +35771,7 @@
35616
35771
  handleActionSelect(event) {
35617
35772
  const { action } = event.detail;
35618
35773
  // Clear any existing command feedback when a new action is selected
35619
- this.clearCommandFeedback();
35774
+ this.clearActionFeedback();
35620
35775
  state.setActiveAction(action);
35621
35776
  if (action === "dictate") {
35622
35777
  this.startDictation();
@@ -35679,10 +35834,12 @@
35679
35834
  if (remainingDelay > 0) {
35680
35835
  setTimeout(() => {
35681
35836
  state.setRecordingState("recording");
35837
+ this.startNoAudioWarningTracking();
35682
35838
  }, remainingDelay);
35683
35839
  }
35684
35840
  else {
35685
35841
  state.setRecordingState("recording");
35842
+ this.startNoAudioWarningTracking();
35686
35843
  }
35687
35844
  },
35688
35845
  });
@@ -35690,7 +35847,10 @@
35690
35847
  catch (error) {
35691
35848
  const errorMessage = error instanceof Error ? error.message : "Connection failed";
35692
35849
  if (errorMessage !== "Disconnected") {
35693
- state.setError(`Failed to connect: ${errorMessage}`);
35850
+ // Only set error if not already in error state (error event may have already set it)
35851
+ if (this.widgetState.recordingState !== "error") {
35852
+ state.setError(`Failed to connect: ${errorMessage}`);
35853
+ }
35694
35854
  await backend.disconnect();
35695
35855
  }
35696
35856
  }
@@ -35745,10 +35905,12 @@
35745
35905
  if (remainingDelay > 0) {
35746
35906
  setTimeout(() => {
35747
35907
  state.setRecordingState("recording");
35908
+ this.startNoAudioWarningTracking();
35748
35909
  }, remainingDelay);
35749
35910
  }
35750
35911
  else {
35751
35912
  state.setRecordingState("recording");
35913
+ this.startNoAudioWarningTracking();
35752
35914
  }
35753
35915
  },
35754
35916
  });
@@ -35756,7 +35918,10 @@
35756
35918
  catch (error) {
35757
35919
  const errorMessage = error instanceof Error ? error.message : "Connection failed";
35758
35920
  if (errorMessage !== "Disconnected") {
35759
- state.setError(`Failed to connect: ${errorMessage}`);
35921
+ // Only set error if not already in error state (error event may have already set it)
35922
+ if (this.widgetState.recordingState !== "error") {
35923
+ state.setError(`Failed to connect: ${errorMessage}`);
35924
+ }
35760
35925
  await backend.disconnect();
35761
35926
  }
35762
35927
  }
@@ -35767,12 +35932,30 @@
35767
35932
  const backend = getBackend();
35768
35933
  try {
35769
35934
  const editedText = await this.withMinDisplayTime(backend.requestEditText(originalContent), 300);
35935
+ // Check if server returned no change (couldn't understand edit)
35936
+ const noChange = editedText.trim() === originalContent.trim();
35937
+ if (noChange) {
35938
+ this.trackActionResult(false);
35939
+ this.showActionFeedback("edit-empty");
35940
+ state.completeRecording();
35941
+ this.editTargetElement = null;
35942
+ this.editSelectionStart = null;
35943
+ this.editSelectionEnd = null;
35944
+ this.editSelectedText = "";
35945
+ backend.disconnect().catch(() => { });
35946
+ backend.startAutoRefresh?.();
35947
+ return;
35948
+ }
35949
+ // Track result - got a meaningful change
35950
+ this.trackActionResult(true);
35770
35951
  this.applyEdit(editedText);
35771
35952
  backend.disconnect().catch(() => { });
35772
35953
  // Start auto-refresh to keep token fresh for subsequent commands (LiveKit only)
35773
35954
  backend.startAutoRefresh?.();
35774
35955
  }
35775
35956
  catch (error) {
35957
+ // Track as failed result
35958
+ this.trackActionResult(false);
35776
35959
  const errorMessage = error instanceof Error ? error.message : "Failed to apply edit";
35777
35960
  if (errorMessage !== "Disconnected") {
35778
35961
  state.setError(errorMessage);
@@ -35798,10 +35981,12 @@
35798
35981
  if (remainingDelay > 0) {
35799
35982
  setTimeout(() => {
35800
35983
  state.setRecordingState("recording");
35984
+ this.startNoAudioWarningTracking();
35801
35985
  }, remainingDelay);
35802
35986
  }
35803
35987
  else {
35804
35988
  state.setRecordingState("recording");
35989
+ this.startNoAudioWarningTracking();
35805
35990
  }
35806
35991
  },
35807
35992
  });
@@ -35809,7 +35994,10 @@
35809
35994
  catch (error) {
35810
35995
  const errorMessage = error instanceof Error ? error.message : "Connection failed";
35811
35996
  if (errorMessage !== "Disconnected") {
35812
- state.setError(`Failed to connect: ${errorMessage}`);
35997
+ // Only set error if not already in error state (error event may have already set it)
35998
+ if (this.widgetState.recordingState !== "error") {
35999
+ state.setError(`Failed to connect: ${errorMessage}`);
36000
+ }
35813
36001
  await backend.disconnect();
35814
36002
  }
35815
36003
  }
@@ -35821,6 +36009,8 @@
35821
36009
  const backend = getBackend();
35822
36010
  try {
35823
36011
  const result = await this.withMinDisplayTime(backend.requestCommand(commands), 300);
36012
+ // Track result - null result means no command matched (possibly no audio)
36013
+ this.trackActionResult(result !== null);
35824
36014
  // Get input text from the backend if available
35825
36015
  const inputText = backend.getLastInputText?.();
35826
36016
  // Save to transcript store
@@ -35838,12 +36028,14 @@
35838
36028
  // Keep widget visible but collapsed (just mic button, no action bubbles)
35839
36029
  state.setState({ isExpanded: false });
35840
36030
  // Show command feedback
35841
- this.showCommandFeedback(result ? "success" : "none");
36031
+ this.showActionFeedback(result ? "command-success" : "command-none");
35842
36032
  backend.disconnect().catch(() => { });
35843
36033
  // Start auto-refresh to keep token fresh for subsequent commands (LiveKit only)
35844
36034
  backend.startAutoRefresh?.();
35845
36035
  }
35846
36036
  catch (error) {
36037
+ // Track as failed result
36038
+ this.trackActionResult(false);
35847
36039
  const errorMessage = error instanceof Error ? error.message : "Failed to process command";
35848
36040
  if (errorMessage !== "Disconnected") {
35849
36041
  state.setError(errorMessage);
@@ -35851,24 +36043,110 @@
35851
36043
  }
35852
36044
  }
35853
36045
  }
35854
- showCommandFeedback(feedback) {
35855
- this.commandFeedback = feedback;
36046
+ showActionFeedback(feedback) {
36047
+ this.actionFeedback = feedback;
35856
36048
  // Clear any existing timeout
35857
- if (this.commandFeedbackTimeout) {
35858
- clearTimeout(this.commandFeedbackTimeout);
36049
+ if (this.actionFeedbackTimeout) {
36050
+ clearTimeout(this.actionFeedbackTimeout);
35859
36051
  }
35860
36052
  // Auto-dismiss after 4 seconds
35861
- this.commandFeedbackTimeout = window.setTimeout(() => {
35862
- this.commandFeedback = null;
35863
- this.commandFeedbackTimeout = null;
36053
+ this.actionFeedbackTimeout = window.setTimeout(() => {
36054
+ this.actionFeedback = null;
36055
+ this.actionFeedbackTimeout = null;
35864
36056
  }, 4000);
35865
36057
  }
35866
- clearCommandFeedback() {
35867
- if (this.commandFeedbackTimeout) {
35868
- clearTimeout(this.commandFeedbackTimeout);
35869
- this.commandFeedbackTimeout = null;
36058
+ clearActionFeedback() {
36059
+ if (this.actionFeedbackTimeout) {
36060
+ clearTimeout(this.actionFeedbackTimeout);
36061
+ this.actionFeedbackTimeout = null;
35870
36062
  }
35871
- this.commandFeedback = null;
36063
+ this.actionFeedback = null;
36064
+ }
36065
+ /**
36066
+ * Start tracking for no-audio warning when recording begins.
36067
+ */
36068
+ startNoAudioWarningTracking() {
36069
+ this.transcriptionReceived = false;
36070
+ this.showNoAudioWarning = false;
36071
+ // If we had consecutive failures, show warning immediately
36072
+ if (this.consecutiveNoAudioActions >= CONSECUTIVE_NO_AUDIO_THRESHOLD) {
36073
+ this.showNoAudioWarning = true;
36074
+ }
36075
+ // Start timeout - if no transcription within 5s, show warning
36076
+ this.noAudioWarningTimeout = window.setTimeout(() => {
36077
+ if (!this.transcriptionReceived &&
36078
+ this.widgetState.recordingState === "recording") {
36079
+ this.showNoAudioWarning = true;
36080
+ }
36081
+ }, NO_AUDIO_WARNING_TIMEOUT_MS);
36082
+ // Subscribe to transcription:interim events
36083
+ this.transcriptionInterimUnsubscribe = events.on("transcription:interim", () => {
36084
+ this.transcriptionReceived = true;
36085
+ if (this.showNoAudioWarning) {
36086
+ this.showNoAudioWarning = false;
36087
+ }
36088
+ });
36089
+ }
36090
+ /**
36091
+ * Clean up no-audio warning tracking when recording stops.
36092
+ */
36093
+ cleanupNoAudioWarningTracking() {
36094
+ if (this.noAudioWarningTimeout !== null) {
36095
+ clearTimeout(this.noAudioWarningTimeout);
36096
+ this.noAudioWarningTimeout = null;
36097
+ }
36098
+ if (this.transcriptionInterimUnsubscribe) {
36099
+ this.transcriptionInterimUnsubscribe();
36100
+ this.transcriptionInterimUnsubscribe = null;
36101
+ }
36102
+ this.showNoAudioWarning = false;
36103
+ }
36104
+ /**
36105
+ * Track the result of an action for consecutive failure detection.
36106
+ */
36107
+ trackActionResult(hasContent) {
36108
+ if (hasContent) {
36109
+ this.consecutiveNoAudioActions = 0;
36110
+ }
36111
+ else {
36112
+ this.consecutiveNoAudioActions++;
36113
+ }
36114
+ }
36115
+ /**
36116
+ * Handle opening settings from the no-audio warning.
36117
+ * Stops the current dictation session immediately, then opens settings.
36118
+ */
36119
+ async handleOpenSettingsFromWarning() {
36120
+ if (getConfig().debug) {
36121
+ console.log("[SpeechOS] No-audio settings link clicked");
36122
+ }
36123
+ // Clean up no-audio warning tracking first
36124
+ this.cleanupNoAudioWarningTracking();
36125
+ // Keep settings open even if widget collapses
36126
+ this.settingsOpenFromWarning = true;
36127
+ // Stop audio capture and disconnect immediately (don't wait for transcription)
36128
+ // Kick this off before opening settings so audio stops fast, but don't block UI.
36129
+ const disconnectPromise = getBackend().disconnect().catch((error) => {
36130
+ if (getConfig().debug) {
36131
+ console.log("[SpeechOS] Disconnect failed while opening settings", error);
36132
+ }
36133
+ });
36134
+ // Update UI state to idle
36135
+ state.cancelRecording();
36136
+ // Clear target elements
36137
+ this.dictationTargetElement = null;
36138
+ this.editTargetElement = null;
36139
+ this.dictationCursorStart = null;
36140
+ this.dictationCursorEnd = null;
36141
+ this.editSelectionStart = null;
36142
+ this.editSelectionEnd = null;
36143
+ this.editSelectedText = "";
36144
+ // Open settings modal
36145
+ this.settingsOpen = true;
36146
+ if (getConfig().debug) {
36147
+ console.log("[SpeechOS] Settings modal opened from no-audio warning");
36148
+ }
36149
+ await disconnectPromise;
35872
36150
  }
35873
36151
  supportsSelection(element) {
35874
36152
  if (element.tagName.toLowerCase() === "textarea") {
@@ -35987,12 +36265,14 @@
35987
36265
  activeAction="${this.widgetState.activeAction || ""}"
35988
36266
  editPreviewText="${this.editSelectedText}"
35989
36267
  errorMessage="${this.widgetState.errorMessage || ""}"
35990
- .commandFeedback="${this.commandFeedback}"
36268
+ .actionFeedback="${this.actionFeedback}"
36269
+ ?showNoAudioWarning="${this.showNoAudioWarning}"
35991
36270
  @mic-click="${this.handleMicClick}"
35992
36271
  @stop-recording="${this.handleStopRecording}"
35993
36272
  @cancel-operation="${this.handleCancelOperation}"
35994
36273
  @retry-connection="${this.handleRetryConnection}"
35995
36274
  @close-widget="${this.handleCloseWidget}"
36275
+ @open-settings="${this.handleOpenSettingsFromWarning}"
35996
36276
  ></speechos-mic-button>
35997
36277
  </div>
35998
36278
  </div>
@@ -36016,7 +36296,10 @@
36016
36296
  ], SpeechOSWidget.prototype, "editHelpModalOpen", void 0);
36017
36297
  __decorate([
36018
36298
  r()
36019
- ], SpeechOSWidget.prototype, "commandFeedback", void 0);
36299
+ ], SpeechOSWidget.prototype, "actionFeedback", void 0);
36300
+ __decorate([
36301
+ r()
36302
+ ], SpeechOSWidget.prototype, "showNoAudioWarning", void 0);
36020
36303
  SpeechOSWidget = SpeechOSWidget_1 = __decorate([
36021
36304
  t$1("speechos-widget")
36022
36305
  ], SpeechOSWidget);