@speechos/client 0.2.6 → 0.2.7

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.
@@ -26910,7 +26910,8 @@
26910
26910
  apiKey: "",
26911
26911
  userId: "",
26912
26912
  host: DEFAULT_HOST,
26913
- debug: false
26913
+ debug: false,
26914
+ webSocketFactory: void 0
26914
26915
  };
26915
26916
  /**
26916
26917
  * Validates and merges user config with defaults
@@ -26923,7 +26924,8 @@
26923
26924
  apiKey: userConfig.apiKey,
26924
26925
  userId: userConfig.userId ?? defaultConfig.userId,
26925
26926
  host: userConfig.host ?? defaultConfig.host,
26926
- debug: userConfig.debug ?? defaultConfig.debug
26927
+ debug: userConfig.debug ?? defaultConfig.debug,
26928
+ webSocketFactory: userConfig.webSocketFactory ?? defaultConfig.webSocketFactory
26927
26929
  };
26928
26930
  }
26929
26931
  /**
@@ -28284,6 +28286,8 @@
28284
28286
  const MESSAGE_TYPE_EXECUTE_COMMAND = "execute_command";
28285
28287
  const MESSAGE_TYPE_COMMAND_RESULT = "command_result";
28286
28288
  const MESSAGE_TYPE_ERROR = "error";
28289
+ const WS_OPEN = 1;
28290
+ const WS_CLOSED = 3;
28287
28291
  /**
28288
28292
  * Response timeout in milliseconds.
28289
28293
  */
@@ -28402,7 +28406,10 @@
28402
28406
  state.setMicEnabled(true);
28403
28407
  const wsUrl = this.getWebSocketUrl();
28404
28408
  if (config.debug) console.log("[SpeechOS] Connecting to WebSocket:", wsUrl);
28405
- this.ws = new WebSocket(wsUrl);
28409
+ this.pendingAuth = new Deferred$1();
28410
+ this.pendingAuth.setTimeout(RESPONSE_TIMEOUT_MS, "Connection timed out", "connection_timeout", "connection");
28411
+ const factory = config.webSocketFactory ?? ((url) => new WebSocket(url));
28412
+ this.ws = factory(wsUrl);
28406
28413
  this.ws.onopen = () => {
28407
28414
  if (config.debug) console.log("[SpeechOS] WebSocket connected, authenticating...");
28408
28415
  this.authenticate();
@@ -28411,19 +28418,21 @@
28411
28418
  this.handleMessage(event.data);
28412
28419
  };
28413
28420
  this.ws.onerror = (event) => {
28414
- console.error("[SpeechOS] WebSocket error:", event);
28421
+ const isConnectionBlocked = this.ws?.readyState === WS_CLOSED;
28422
+ const errorCode = isConnectionBlocked ? "connection_blocked" : "websocket_error";
28423
+ const errorMessage = isConnectionBlocked ? "This site's CSP blocks the extension. Try embedded mode instead." : "WebSocket connection error";
28424
+ console.error("[SpeechOS] WebSocket error:", event, { isConnectionBlocked });
28415
28425
  events.emit("error", {
28416
- code: "websocket_error",
28417
- message: "WebSocket connection error",
28426
+ code: errorCode,
28427
+ message: errorMessage,
28418
28428
  source: "connection"
28419
28429
  });
28430
+ if (this.pendingAuth) this.pendingAuth.reject(new Error(errorMessage));
28420
28431
  };
28421
28432
  this.ws.onclose = (event) => {
28422
28433
  if (config.debug) console.log("[SpeechOS] WebSocket closed:", event.code, event.reason);
28423
28434
  state.setConnected(false);
28424
28435
  };
28425
- this.pendingAuth = new Deferred$1();
28426
- this.pendingAuth.setTimeout(RESPONSE_TIMEOUT_MS, "Connection timed out", "connection_timeout", "connection");
28427
28436
  await this.pendingAuth.promise;
28428
28437
  this.pendingAuth = null;
28429
28438
  if (this.audioCapture) this.audioCapture.setReady();
@@ -28472,7 +28481,7 @@
28472
28481
  * Actually send the audio chunk (async operation).
28473
28482
  */
28474
28483
  async doSendAudioChunk(chunk) {
28475
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
28484
+ if (this.ws && this.ws.readyState === WS_OPEN) {
28476
28485
  const arrayBuffer = await chunk.arrayBuffer();
28477
28486
  this.ws.send(arrayBuffer);
28478
28487
  }
@@ -28658,7 +28667,7 @@
28658
28667
  * the transcript. Uses the same pattern as LiveKit's ReadableStream approach.
28659
28668
  */
28660
28669
  async waitForBufferDrain() {
28661
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
28670
+ if (!this.ws || this.ws.readyState !== WS_OPEN) return;
28662
28671
  const config = getConfig();
28663
28672
  const startTime = Date.now();
28664
28673
  while (this.ws.bufferedAmount > 0) {
@@ -28674,7 +28683,7 @@
28674
28683
  * Send a JSON message over the WebSocket.
28675
28684
  */
28676
28685
  sendMessage(message) {
28677
- if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(message));
28686
+ if (this.ws && this.ws.readyState === WS_OPEN) this.ws.send(JSON.stringify(message));
28678
28687
  }
28679
28688
  /**
28680
28689
  * Disconnect from the WebSocket.
@@ -28716,7 +28725,7 @@
28716
28725
  * Check if connected to WebSocket.
28717
28726
  */
28718
28727
  isConnected() {
28719
- return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
28728
+ return this.ws !== null && this.ws.readyState === WS_OPEN;
28720
28729
  }
28721
28730
  /**
28722
28731
  * Get the last input text from a command result.
@@ -28897,6 +28906,10 @@
28897
28906
  this.focusHandler = (event) => {
28898
28907
  const target = event.target;
28899
28908
  if (isFormField(target)) {
28909
+ console.log("[SpeechOS] FormDetector: focus on form field", {
28910
+ element: target,
28911
+ tagName: target?.tagName,
28912
+ });
28900
28913
  state.setFocusedElement(target);
28901
28914
  state.show();
28902
28915
  events.emit("form:focus", { element: target });
@@ -30152,6 +30165,71 @@
30152
30165
  deleteTranscript: deleteTranscript,
30153
30166
  };
30154
30167
 
30168
+ function isNativeField(field) {
30169
+ return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement;
30170
+ }
30171
+ /** Call a function after focusing a field and then restore the previous focus afterwards if necessary */
30172
+ function withFocus(field, callback) {
30173
+ const document = field.ownerDocument;
30174
+ const initialFocus = document.activeElement;
30175
+ if (initialFocus === field) {
30176
+ return callback();
30177
+ }
30178
+ try {
30179
+ field.focus();
30180
+ return callback();
30181
+ }
30182
+ finally {
30183
+ field.blur(); // Supports `intialFocus === body`
30184
+ if (initialFocus instanceof HTMLElement) {
30185
+ initialFocus.focus();
30186
+ }
30187
+ }
30188
+ }
30189
+ // This will insert into the focused field. It shouild always be called inside withFocus.
30190
+ // Use this one locally if there are multiple `insertTextIntoField` or `document.execCommand` calls
30191
+ function insertTextWhereverTheFocusIs(document, text) {
30192
+ if (text === '') {
30193
+ // https://github.com/fregante/text-field-edit/issues/16
30194
+ document.execCommand('delete');
30195
+ }
30196
+ else {
30197
+ document.execCommand('insertText', false, text);
30198
+ }
30199
+ }
30200
+ /** Inserts `text` at the cursor’s position, replacing any selection, with **undo** support and by firing the `input` event. */
30201
+ function insertTextIntoField(field, text) {
30202
+ withFocus(field, () => {
30203
+ insertTextWhereverTheFocusIs(field.ownerDocument, text);
30204
+ });
30205
+ }
30206
+ /** Replaces the entire content, equivalent to `field.value = text` but with **undo** support and by firing the `input` event. */
30207
+ function setFieldText(field, text) {
30208
+ if (isNativeField(field)) {
30209
+ field.select();
30210
+ insertTextIntoField(field, text);
30211
+ }
30212
+ else {
30213
+ const document = field.ownerDocument;
30214
+ withFocus(field, () => {
30215
+ document.execCommand('selectAll', false, text);
30216
+ insertTextWhereverTheFocusIs(document, text);
30217
+ });
30218
+ }
30219
+ }
30220
+ /** Get the selected text in a field or an empty string if nothing is selected. */
30221
+ function getFieldSelection(field) {
30222
+ if (isNativeField(field)) {
30223
+ return field.value.slice(field.selectionStart, field.selectionEnd);
30224
+ }
30225
+ const selection = field.ownerDocument.getSelection();
30226
+ if (selection && field.contains(selection.anchorNode)) {
30227
+ // The selection is inside the field
30228
+ return selection.toString();
30229
+ }
30230
+ return '';
30231
+ }
30232
+
30155
30233
  /**
30156
30234
  * @license
30157
30235
  * Copyright 2017 Google LLC
@@ -30512,6 +30590,7 @@
30512
30590
  this.activeAction = null;
30513
30591
  this.editPreviewText = "";
30514
30592
  this.errorMessage = null;
30593
+ this.showRetryButton = true;
30515
30594
  this.actionFeedback = null;
30516
30595
  this.showNoAudioWarning = false;
30517
30596
  }
@@ -31221,10 +31300,14 @@
31221
31300
  bottom: 72px; /* Above button */
31222
31301
  left: 50%;
31223
31302
  transform: translateX(-50%) translateY(8px);
31303
+ min-width: 200px;
31224
31304
  max-width: 280px;
31305
+ width: max-content;
31225
31306
  font-size: 13px;
31226
31307
  color: white;
31227
31308
  white-space: normal;
31309
+ word-wrap: break-word;
31310
+ overflow-wrap: break-word;
31228
31311
  text-align: center;
31229
31312
  padding: 12px 16px;
31230
31313
  border-radius: 12px;
@@ -31437,6 +31520,7 @@
31437
31520
  .error-message {
31438
31521
  font-size: 15px;
31439
31522
  padding: 14px 18px;
31523
+ min-width: 220px;
31440
31524
  max-width: 300px;
31441
31525
  bottom: 94px;
31442
31526
  }
@@ -31662,9 +31746,13 @@
31662
31746
  ? b `
31663
31747
  <div class="error-message ${showError ? "visible" : ""}">
31664
31748
  ${this.errorMessage}
31665
- <button class="retry-button" @click="${this.handleRetry}">
31666
- Retry Connection
31667
- </button>
31749
+ ${this.showRetryButton
31750
+ ? b `
31751
+ <button class="retry-button" @click="${this.handleRetry}">
31752
+ Retry Connection
31753
+ </button>
31754
+ `
31755
+ : ""}
31668
31756
  </div>
31669
31757
  `
31670
31758
  : ""}
@@ -31763,6 +31851,9 @@
31763
31851
  __decorate([
31764
31852
  n({ type: String })
31765
31853
  ], SpeechOSMicButton.prototype, "errorMessage", void 0);
31854
+ __decorate([
31855
+ n({ type: Boolean })
31856
+ ], SpeechOSMicButton.prototype, "showRetryButton", void 0);
31766
31857
  __decorate([
31767
31858
  n({ type: String })
31768
31859
  ], SpeechOSMicButton.prototype, "actionFeedback", void 0);
@@ -34798,6 +34889,7 @@
34798
34889
  super(...arguments);
34799
34890
  this.open = false;
34800
34891
  this.text = "";
34892
+ this.mode = "dictation";
34801
34893
  this.copied = false;
34802
34894
  this.copyTimeout = null;
34803
34895
  }
@@ -34877,6 +34969,41 @@
34877
34969
  color: #10b981;
34878
34970
  flex-shrink: 0;
34879
34971
  }
34972
+
34973
+ /* Edit mode styles */
34974
+ :host([mode="edit"]) .logo-icon {
34975
+ background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
34976
+ }
34977
+
34978
+ :host([mode="edit"]) .modal-title {
34979
+ background: linear-gradient(135deg, #a78bfa 0%, #818cf8 100%);
34980
+ -webkit-background-clip: text;
34981
+ -webkit-text-fill-color: transparent;
34982
+ background-clip: text;
34983
+ }
34984
+
34985
+ :host([mode="edit"]) .hint {
34986
+ background: rgba(139, 92, 246, 0.08);
34987
+ }
34988
+
34989
+ :host([mode="edit"]) .hint-icon {
34990
+ color: #8b5cf6;
34991
+ }
34992
+
34993
+ :host([mode="edit"]) .btn-primary {
34994
+ background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
34995
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
34996
+ }
34997
+
34998
+ :host([mode="edit"]) .btn-primary:hover {
34999
+ background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
35000
+ box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
35001
+ }
35002
+
35003
+ :host([mode="edit"]) .btn-success {
35004
+ background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
35005
+ box-shadow: 0 4px 12px rgba(167, 139, 250, 0.3);
35006
+ }
34880
35007
  `,
34881
35008
  ]; }
34882
35009
  disconnectedCallback() {
@@ -34929,6 +35056,17 @@
34929
35056
  console.error("[SpeechOS] Failed to copy text:", err);
34930
35057
  }
34931
35058
  }
35059
+ get modalTitle() {
35060
+ return this.mode === "edit" ? "Edit Complete" : "Dictation Complete";
35061
+ }
35062
+ get modalIcon() {
35063
+ return this.mode === "edit" ? editIcon(18) : micIcon(18);
35064
+ }
35065
+ get hintText() {
35066
+ return this.mode === "edit"
35067
+ ? "Tip: The editor didn't accept the edit. Copy and paste manually."
35068
+ : "Tip: Focus a text field first to auto-insert next time";
35069
+ }
34932
35070
  render() {
34933
35071
  return b `
34934
35072
  <div
@@ -34938,8 +35076,8 @@
34938
35076
  <div class="modal-card">
34939
35077
  <div class="modal-header">
34940
35078
  <div class="header-content">
34941
- <div class="logo-icon">${micIcon(18)}</div>
34942
- <h2 class="modal-title">Dictation Complete</h2>
35079
+ <div class="logo-icon">${this.modalIcon}</div>
35080
+ <h2 class="modal-title">${this.modalTitle}</h2>
34943
35081
  </div>
34944
35082
  <button
34945
35083
  class="close-button"
@@ -34956,7 +35094,7 @@
34956
35094
  <svg class="hint-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
34957
35095
  <circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
34958
35096
  </svg>
34959
- <span>Tip: Focus a text field first to auto-insert next time</span>
35097
+ <span>${this.hintText}</span>
34960
35098
  </div>
34961
35099
  </div>
34962
35100
 
@@ -34983,6 +35121,9 @@
34983
35121
  __decorate([
34984
35122
  n({ type: String })
34985
35123
  ], SpeechOSDictationOutputModal.prototype, "text", void 0);
35124
+ __decorate([
35125
+ n({ type: String, reflect: true })
35126
+ ], SpeechOSDictationOutputModal.prototype, "mode", void 0);
34986
35127
  __decorate([
34987
35128
  r()
34988
35129
  ], SpeechOSDictationOutputModal.prototype, "copied", void 0);
@@ -35160,9 +35301,11 @@
35160
35301
  this.settingsOpenFromWarning = false;
35161
35302
  this.dictationModalOpen = false;
35162
35303
  this.dictationModalText = "";
35304
+ this.dictationModalMode = "dictation";
35163
35305
  this.editHelpModalOpen = false;
35164
35306
  this.actionFeedback = null;
35165
35307
  this.showNoAudioWarning = false;
35308
+ this.isErrorRetryable = true;
35166
35309
  this.dictationTargetElement = null;
35167
35310
  this.editTargetElement = null;
35168
35311
  this.dictationCursorStart = null;
@@ -35311,6 +35454,8 @@
35311
35454
  this.errorEventUnsubscribe = events.on("error", (payload) => {
35312
35455
  if (this.widgetState.recordingState !== "idle" &&
35313
35456
  this.widgetState.recordingState !== "error") {
35457
+ // Check if this is a non-retryable error (e.g., CSP blocked connection)
35458
+ this.isErrorRetryable = payload.code !== "connection_blocked";
35314
35459
  state.setError(payload.message);
35315
35460
  getBackend().disconnect().catch(() => { });
35316
35461
  }
@@ -35385,6 +35530,9 @@
35385
35530
  if (changedProperties.has("dictationModalText") && this.dictationModalElement) {
35386
35531
  this.dictationModalElement.text = this.dictationModalText;
35387
35532
  }
35533
+ if (changedProperties.has("dictationModalMode") && this.dictationModalElement) {
35534
+ this.dictationModalElement.mode = this.dictationModalMode;
35535
+ }
35388
35536
  if (changedProperties.has("editHelpModalOpen") && this.editHelpModalElement) {
35389
35537
  this.editHelpModalElement.open = this.editHelpModalOpen;
35390
35538
  }
@@ -35583,13 +35731,24 @@
35583
35731
  // Track result for consecutive failure detection
35584
35732
  this.trackActionResult(!!transcription);
35585
35733
  if (transcription) {
35734
+ if (getConfig().debug) {
35735
+ console.log("[SpeechOS] Transcription received:", {
35736
+ transcription,
35737
+ dictationTargetElement: this.dictationTargetElement,
35738
+ tagName: this.dictationTargetElement?.tagName,
35739
+ });
35740
+ }
35586
35741
  // Check if we have a target element to insert into
35587
35742
  if (this.dictationTargetElement) {
35588
35743
  this.insertTranscription(transcription);
35589
35744
  }
35590
35745
  else {
35591
35746
  // No target element - show dictation output modal
35747
+ if (getConfig().debug) {
35748
+ console.log("[SpeechOS] No target element, showing dictation modal");
35749
+ }
35592
35750
  this.dictationModalText = transcription;
35751
+ this.dictationModalMode = "dictation";
35593
35752
  this.dictationModalOpen = true;
35594
35753
  }
35595
35754
  transcriptStore.saveTranscript(transcription, "dictate");
@@ -35733,41 +35892,66 @@
35733
35892
  return;
35734
35893
  }
35735
35894
  const tagName = target.tagName.toLowerCase();
35895
+ const originalContent = this.getElementContent(target) || "";
35736
35896
  if (tagName === "input" || tagName === "textarea") {
35737
35897
  const inputEl = target;
35898
+ // Restore cursor position before inserting
35738
35899
  const start = this.dictationCursorStart ?? inputEl.value.length;
35739
35900
  const end = this.dictationCursorEnd ?? inputEl.value.length;
35740
- const before = inputEl.value.substring(0, start);
35741
- const after = inputEl.value.substring(end);
35742
- inputEl.value = before + text + after;
35743
- if (this.supportsSelection(inputEl)) {
35744
- const newCursorPos = start + text.length;
35745
- inputEl.setSelectionRange(newCursorPos, newCursorPos);
35746
- }
35747
- inputEl.dispatchEvent(new Event("input", { bubbles: true }));
35748
- inputEl.focus();
35901
+ inputEl.setSelectionRange(start, end);
35902
+ // Use text-field-edit to insert text (handles undo, events, etc.)
35903
+ insertTextIntoField(inputEl, text);
35749
35904
  state.setFocusedElement(inputEl);
35750
35905
  }
35751
35906
  else if (target.isContentEditable) {
35752
35907
  target.focus();
35753
35908
  state.setFocusedElement(target);
35754
- const textNode = document.createTextNode(text);
35755
- target.appendChild(textNode);
35756
- const selection = window.getSelection();
35757
- if (selection) {
35758
- const range = document.createRange();
35759
- range.selectNodeContents(textNode);
35760
- range.collapse(false);
35761
- selection.removeAllRanges();
35762
- selection.addRange(range);
35763
- }
35764
- target.dispatchEvent(new Event("input", { bubbles: true }));
35909
+ // Use text-field-edit for contentEditable elements
35910
+ insertTextIntoField(target, text);
35765
35911
  }
35766
35912
  events.emit("transcription:inserted", { text, element: target });
35913
+ // Verify insertion was applied after DOM updates
35914
+ this.verifyInsertionApplied(target, text, originalContent);
35767
35915
  this.dictationTargetElement = null;
35768
35916
  this.dictationCursorStart = null;
35769
35917
  this.dictationCursorEnd = null;
35770
35918
  }
35919
+ /**
35920
+ * Verify that a dictation insertion was actually applied to the target element.
35921
+ * Some custom editors (CodeMirror, Monaco, Slate, etc.) don't respond to
35922
+ * standard DOM editing methods. If the insertion fails, show a fallback modal.
35923
+ */
35924
+ verifyInsertionApplied(target, insertedText, originalContent) {
35925
+ // Use requestAnimationFrame to check after DOM updates
35926
+ requestAnimationFrame(() => {
35927
+ const tagName = target.tagName.toLowerCase();
35928
+ let currentContent = "";
35929
+ if (tagName === "input" || tagName === "textarea") {
35930
+ currentContent = target.value;
35931
+ }
35932
+ else if (target.isContentEditable) {
35933
+ currentContent = target.textContent || "";
35934
+ }
35935
+ // Check if the insertion was applied:
35936
+ // - Content should contain the inserted text
35937
+ // - Or content should be different from original (for empty fields)
35938
+ const insertionApplied = currentContent.includes(insertedText) ||
35939
+ (originalContent === "" && currentContent !== "");
35940
+ if (!insertionApplied) {
35941
+ if (getConfig().debug) {
35942
+ console.log("[SpeechOS] Dictation failed to insert, showing fallback modal", {
35943
+ insertedText,
35944
+ currentContent,
35945
+ originalContent,
35946
+ });
35947
+ }
35948
+ // Show fallback modal with dictation mode styling
35949
+ this.dictationModalText = insertedText;
35950
+ this.dictationModalMode = "dictation";
35951
+ this.dictationModalOpen = true;
35952
+ }
35953
+ });
35954
+ }
35771
35955
  handleActionSelect(event) {
35772
35956
  const { action } = event.detail;
35773
35957
  // Clear any existing command feedback when a new action is selected
@@ -35806,6 +35990,13 @@
35806
35990
  this.dictationTargetElement = this.widgetState.focusedElement;
35807
35991
  this.dictationCursorStart = null;
35808
35992
  this.dictationCursorEnd = null;
35993
+ if (getConfig().debug) {
35994
+ console.log("[SpeechOS] startDictation:", {
35995
+ focusedElement: this.widgetState.focusedElement,
35996
+ dictationTargetElement: this.dictationTargetElement,
35997
+ tagName: this.dictationTargetElement?.tagName,
35998
+ });
35999
+ }
35809
36000
  if (this.dictationTargetElement) {
35810
36001
  const tagName = this.dictationTargetElement.tagName.toLowerCase();
35811
36002
  if (tagName === "input" || tagName === "textarea") {
@@ -35831,15 +36022,18 @@
35831
36022
  // Ensure minimum animation duration before transitioning to recording
35832
36023
  const elapsed = Date.now() - connectingStartTime;
35833
36024
  const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
36025
+ const startRecording = () => {
36026
+ if (state.getState().recordingState === "error") {
36027
+ return;
36028
+ }
36029
+ state.setRecordingState("recording");
36030
+ this.startNoAudioWarningTracking();
36031
+ };
35834
36032
  if (remainingDelay > 0) {
35835
- setTimeout(() => {
35836
- state.setRecordingState("recording");
35837
- this.startNoAudioWarningTracking();
35838
- }, remainingDelay);
36033
+ setTimeout(startRecording, remainingDelay);
35839
36034
  }
35840
36035
  else {
35841
- state.setRecordingState("recording");
35842
- this.startNoAudioWarningTracking();
36036
+ startRecording();
35843
36037
  }
35844
36038
  },
35845
36039
  });
@@ -35860,6 +36054,13 @@
35860
36054
  this.editSelectionStart = null;
35861
36055
  this.editSelectionEnd = null;
35862
36056
  this.editSelectedText = "";
36057
+ if (getConfig().debug) {
36058
+ console.log("[SpeechOS] startEdit:", {
36059
+ focusedElement: this.widgetState.focusedElement,
36060
+ editTargetElement: this.editTargetElement,
36061
+ tagName: this.editTargetElement?.tagName,
36062
+ });
36063
+ }
35863
36064
  if (this.editTargetElement) {
35864
36065
  const tagName = this.editTargetElement.tagName.toLowerCase();
35865
36066
  if (tagName === "input" || tagName === "textarea") {
@@ -35870,7 +36071,8 @@
35870
36071
  const start = this.editSelectionStart ?? 0;
35871
36072
  const end = this.editSelectionEnd ?? 0;
35872
36073
  if (start !== end) {
35873
- this.editSelectedText = inputEl.value.substring(start, end);
36074
+ // Use getFieldSelection from text-field-edit
36075
+ this.editSelectedText = getFieldSelection(inputEl);
35874
36076
  }
35875
36077
  }
35876
36078
  else {
@@ -35879,13 +36081,11 @@
35879
36081
  }
35880
36082
  }
35881
36083
  else if (this.editTargetElement.isContentEditable) {
35882
- const selection = window.getSelection();
35883
- if (selection && selection.rangeCount > 0) {
35884
- const selectedText = selection.toString();
35885
- this.editSelectionStart = 0;
35886
- this.editSelectionEnd = selectedText.length;
35887
- this.editSelectedText = selectedText;
35888
- }
36084
+ // Use getFieldSelection from text-field-edit for contentEditable too
36085
+ const selectedText = getFieldSelection(this.editTargetElement);
36086
+ this.editSelectionStart = 0;
36087
+ this.editSelectionEnd = selectedText.length;
36088
+ this.editSelectedText = selectedText;
35889
36089
  }
35890
36090
  }
35891
36091
  // Capture the content to edit at start time (sent with auth message)
@@ -35902,15 +36102,18 @@
35902
36102
  // Ensure minimum animation duration before transitioning to recording
35903
36103
  const elapsed = Date.now() - connectingStartTime;
35904
36104
  const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
36105
+ const startRecording = () => {
36106
+ if (state.getState().recordingState === "error") {
36107
+ return;
36108
+ }
36109
+ state.setRecordingState("recording");
36110
+ this.startNoAudioWarningTracking();
36111
+ };
35905
36112
  if (remainingDelay > 0) {
35906
- setTimeout(() => {
35907
- state.setRecordingState("recording");
35908
- this.startNoAudioWarningTracking();
35909
- }, remainingDelay);
36113
+ setTimeout(startRecording, remainingDelay);
35910
36114
  }
35911
36115
  else {
35912
- state.setRecordingState("recording");
35913
- this.startNoAudioWarningTracking();
36116
+ startRecording();
35914
36117
  }
35915
36118
  },
35916
36119
  });
@@ -35978,15 +36181,18 @@
35978
36181
  // Ensure minimum animation duration before transitioning to recording
35979
36182
  const elapsed = Date.now() - connectingStartTime;
35980
36183
  const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
36184
+ const startRecording = () => {
36185
+ if (state.getState().recordingState === "error") {
36186
+ return;
36187
+ }
36188
+ state.setRecordingState("recording");
36189
+ this.startNoAudioWarningTracking();
36190
+ };
35981
36191
  if (remainingDelay > 0) {
35982
- setTimeout(() => {
35983
- state.setRecordingState("recording");
35984
- this.startNoAudioWarningTracking();
35985
- }, remainingDelay);
36192
+ setTimeout(startRecording, remainingDelay);
35986
36193
  }
35987
36194
  else {
35988
- state.setRecordingState("recording");
35989
- this.startNoAudioWarningTracking();
36195
+ startRecording();
35990
36196
  }
35991
36197
  },
35992
36198
  });
@@ -36162,21 +36368,14 @@
36162
36368
  const tagName = element.tagName.toLowerCase();
36163
36369
  if (tagName === "input" || tagName === "textarea") {
36164
36370
  const inputEl = element;
36165
- const fullContent = inputEl.value;
36166
- const start = this.editSelectionStart ?? 0;
36167
- const end = this.editSelectionEnd ?? fullContent.length;
36168
- const hasSelection = start !== end;
36169
- if (hasSelection) {
36170
- return fullContent.substring(start, end);
36171
- }
36172
- return fullContent;
36371
+ const selectedText = getFieldSelection(inputEl);
36372
+ // If there's selected text, return it; otherwise return full content
36373
+ return selectedText || inputEl.value;
36173
36374
  }
36174
36375
  else if (element.isContentEditable) {
36175
- const selection = window.getSelection();
36176
- if (selection && selection.toString().length > 0) {
36177
- return selection.toString();
36178
- }
36179
- return element.textContent || "";
36376
+ const selectedText = getFieldSelection(element);
36377
+ // If there's selected text, return it; otherwise return full content
36378
+ return selectedText || element.textContent || "";
36180
36379
  }
36181
36380
  return "";
36182
36381
  }
@@ -36191,40 +36390,44 @@
36191
36390
  if (tagName === "input" || tagName === "textarea") {
36192
36391
  const inputEl = target;
36193
36392
  originalContent = inputEl.value;
36194
- inputEl.focus();
36195
- if (this.supportsSelection(inputEl)) {
36196
- const selectionStart = this.editSelectionStart ?? 0;
36197
- const selectionEnd = this.editSelectionEnd ?? inputEl.value.length;
36198
- const hasSelection = selectionStart !== selectionEnd;
36199
- if (hasSelection) {
36200
- inputEl.setSelectionRange(selectionStart, selectionEnd);
36201
- }
36202
- else {
36203
- inputEl.setSelectionRange(0, inputEl.value.length);
36204
- }
36205
- document.execCommand("insertText", false, editedText);
36393
+ // Restore the original selection/cursor position
36394
+ const selectionStart = this.editSelectionStart ?? 0;
36395
+ const selectionEnd = this.editSelectionEnd ?? inputEl.value.length;
36396
+ const hasSelection = selectionStart !== selectionEnd;
36397
+ if (hasSelection) {
36398
+ // Restore selection, then use insertTextIntoField() to replace it
36399
+ inputEl.setSelectionRange(selectionStart, selectionEnd);
36400
+ insertTextIntoField(inputEl, editedText);
36206
36401
  }
36207
36402
  else {
36208
- inputEl.value = editedText;
36209
- inputEl.dispatchEvent(new Event("input", { bubbles: true }));
36403
+ // No selection - replace entire content using setFieldText()
36404
+ setFieldText(inputEl, editedText);
36210
36405
  }
36211
36406
  state.setFocusedElement(inputEl);
36212
36407
  }
36213
36408
  else if (target.isContentEditable) {
36214
36409
  originalContent = target.textContent || "";
36215
- target.focus();
36216
- state.setFocusedElement(target);
36217
36410
  const hasSelection = this.editSelectionStart !== null &&
36218
36411
  this.editSelectionEnd !== null &&
36219
36412
  this.editSelectionStart !== this.editSelectionEnd;
36220
- if (!hasSelection) {
36413
+ if (hasSelection) {
36414
+ // Selection exists - focus and insert (assumes selection is still active or we restore it)
36415
+ target.focus();
36416
+ insertTextIntoField(target, editedText);
36417
+ }
36418
+ else {
36419
+ // No selection - select all content first, then replace with insertTextIntoField()
36420
+ target.focus();
36221
36421
  const selection = window.getSelection();
36222
- const range = document.createRange();
36223
- range.selectNodeContents(target);
36224
- selection?.removeAllRanges();
36225
- selection?.addRange(range);
36422
+ if (selection) {
36423
+ const range = document.createRange();
36424
+ range.selectNodeContents(target);
36425
+ selection.removeAllRanges();
36426
+ selection.addRange(range);
36427
+ }
36428
+ insertTextIntoField(target, editedText);
36226
36429
  }
36227
- document.execCommand("insertText", false, editedText);
36430
+ state.setFocusedElement(target);
36228
36431
  }
36229
36432
  transcriptStore.saveTranscript(editedText, "edit", originalContent);
36230
36433
  events.emit("edit:applied", {
@@ -36233,11 +36436,54 @@
36233
36436
  element: target,
36234
36437
  });
36235
36438
  state.completeRecording();
36439
+ // Verify edit was applied after DOM updates
36440
+ this.verifyEditApplied(target, editedText, originalContent);
36236
36441
  this.editTargetElement = null;
36237
36442
  this.editSelectionStart = null;
36238
36443
  this.editSelectionEnd = null;
36239
36444
  this.editSelectedText = "";
36240
36445
  }
36446
+ /**
36447
+ * Verify that an edit was actually applied to the target element.
36448
+ * Some custom editors (CodeMirror, Monaco, Slate, etc.) don't respond to
36449
+ * standard DOM editing methods. If the edit fails, show a fallback modal.
36450
+ */
36451
+ verifyEditApplied(target, editedText, originalContent) {
36452
+ // Use requestAnimationFrame to check after DOM updates
36453
+ requestAnimationFrame(() => {
36454
+ const tagName = target.tagName.toLowerCase();
36455
+ let currentContent = "";
36456
+ if (tagName === "input" || tagName === "textarea") {
36457
+ currentContent = target.value;
36458
+ }
36459
+ else if (target.isContentEditable) {
36460
+ currentContent = target.textContent || "";
36461
+ }
36462
+ // Normalize whitespace for comparison
36463
+ const normalizedCurrent = currentContent.trim();
36464
+ const normalizedEdited = editedText.trim();
36465
+ const normalizedOriginal = originalContent.trim();
36466
+ // Check if the edit was applied:
36467
+ // - Content should be different from original (unless edit was no-op)
36468
+ // - Content should contain or match the edited text
36469
+ const editApplied = normalizedCurrent !== normalizedOriginal ||
36470
+ normalizedCurrent === normalizedEdited ||
36471
+ normalizedCurrent.includes(normalizedEdited);
36472
+ if (!editApplied) {
36473
+ if (getConfig().debug) {
36474
+ console.log("[SpeechOS] Edit failed to apply, showing fallback modal", {
36475
+ expected: editedText,
36476
+ actual: currentContent,
36477
+ original: originalContent,
36478
+ });
36479
+ }
36480
+ // Show fallback modal with edit mode styling
36481
+ this.dictationModalText = editedText;
36482
+ this.dictationModalMode = "edit";
36483
+ this.dictationModalOpen = true;
36484
+ }
36485
+ });
36486
+ }
36241
36487
  render() {
36242
36488
  if (!this.widgetState.isVisible) {
36243
36489
  this.setAttribute("hidden", "");
@@ -36265,6 +36511,7 @@
36265
36511
  activeAction="${this.widgetState.activeAction || ""}"
36266
36512
  editPreviewText="${this.editSelectedText}"
36267
36513
  errorMessage="${this.widgetState.errorMessage || ""}"
36514
+ ?showRetryButton="${this.isErrorRetryable}"
36268
36515
  .actionFeedback="${this.actionFeedback}"
36269
36516
  ?showNoAudioWarning="${this.showNoAudioWarning}"
36270
36517
  @mic-click="${this.handleMicClick}"
@@ -36291,6 +36538,9 @@
36291
36538
  __decorate([
36292
36539
  r()
36293
36540
  ], SpeechOSWidget.prototype, "dictationModalText", void 0);
36541
+ __decorate([
36542
+ r()
36543
+ ], SpeechOSWidget.prototype, "dictationModalMode", void 0);
36294
36544
  __decorate([
36295
36545
  r()
36296
36546
  ], SpeechOSWidget.prototype, "editHelpModalOpen", void 0);
@@ -36300,6 +36550,9 @@
36300
36550
  __decorate([
36301
36551
  r()
36302
36552
  ], SpeechOSWidget.prototype, "showNoAudioWarning", void 0);
36553
+ __decorate([
36554
+ r()
36555
+ ], SpeechOSWidget.prototype, "isErrorRetryable", void 0);
36303
36556
  SpeechOSWidget = SpeechOSWidget_1 = __decorate([
36304
36557
  t$1("speechos-widget")
36305
36558
  ], SpeechOSWidget);