@speechos/client 0.2.6 → 0.2.8
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/form-detector.d.ts.map +1 -1
- package/dist/index.cjs +334 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.iife.js +359 -102
- package/dist/index.iife.js.map +1 -1
- package/dist/index.iife.min.js +105 -62
- package/dist/index.iife.min.js.map +1 -1
- package/dist/index.js +334 -88
- package/dist/index.js.map +1 -1
- package/dist/ui/dictation-output-modal.d.ts +5 -0
- package/dist/ui/dictation-output-modal.d.ts.map +1 -1
- package/dist/ui/mic-button.d.ts +1 -0
- package/dist/ui/mic-button.d.ts.map +1 -1
- package/dist/ui/widget.d.ts +14 -0
- package/dist/ui/widget.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -142,6 +142,10 @@ class FormDetector {
|
|
|
142
142
|
this.focusHandler = (event) => {
|
|
143
143
|
const target = event.target;
|
|
144
144
|
if (isFormField(target)) {
|
|
145
|
+
console.log("[SpeechOS] FormDetector: focus on form field", {
|
|
146
|
+
element: target,
|
|
147
|
+
tagName: target?.tagName,
|
|
148
|
+
});
|
|
145
149
|
state.setFocusedElement(target);
|
|
146
150
|
state.show();
|
|
147
151
|
events.emit("form:focus", { element: target });
|
|
@@ -1397,6 +1401,71 @@ const transcriptStore = {
|
|
|
1397
1401
|
deleteTranscript: deleteTranscript,
|
|
1398
1402
|
};
|
|
1399
1403
|
|
|
1404
|
+
function isNativeField(field) {
|
|
1405
|
+
return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement;
|
|
1406
|
+
}
|
|
1407
|
+
/** Call a function after focusing a field and then restore the previous focus afterwards if necessary */
|
|
1408
|
+
function withFocus(field, callback) {
|
|
1409
|
+
const document = field.ownerDocument;
|
|
1410
|
+
const initialFocus = document.activeElement;
|
|
1411
|
+
if (initialFocus === field) {
|
|
1412
|
+
return callback();
|
|
1413
|
+
}
|
|
1414
|
+
try {
|
|
1415
|
+
field.focus();
|
|
1416
|
+
return callback();
|
|
1417
|
+
}
|
|
1418
|
+
finally {
|
|
1419
|
+
field.blur(); // Supports `intialFocus === body`
|
|
1420
|
+
if (initialFocus instanceof HTMLElement) {
|
|
1421
|
+
initialFocus.focus();
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
// This will insert into the focused field. It shouild always be called inside withFocus.
|
|
1426
|
+
// Use this one locally if there are multiple `insertTextIntoField` or `document.execCommand` calls
|
|
1427
|
+
function insertTextWhereverTheFocusIs(document, text) {
|
|
1428
|
+
if (text === '') {
|
|
1429
|
+
// https://github.com/fregante/text-field-edit/issues/16
|
|
1430
|
+
document.execCommand('delete');
|
|
1431
|
+
}
|
|
1432
|
+
else {
|
|
1433
|
+
document.execCommand('insertText', false, text);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
/** Inserts `text` at the cursor’s position, replacing any selection, with **undo** support and by firing the `input` event. */
|
|
1437
|
+
function insertTextIntoField(field, text) {
|
|
1438
|
+
withFocus(field, () => {
|
|
1439
|
+
insertTextWhereverTheFocusIs(field.ownerDocument, text);
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
/** Replaces the entire content, equivalent to `field.value = text` but with **undo** support and by firing the `input` event. */
|
|
1443
|
+
function setFieldText(field, text) {
|
|
1444
|
+
if (isNativeField(field)) {
|
|
1445
|
+
field.select();
|
|
1446
|
+
insertTextIntoField(field, text);
|
|
1447
|
+
}
|
|
1448
|
+
else {
|
|
1449
|
+
const document = field.ownerDocument;
|
|
1450
|
+
withFocus(field, () => {
|
|
1451
|
+
document.execCommand('selectAll', false, text);
|
|
1452
|
+
insertTextWhereverTheFocusIs(document, text);
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
/** Get the selected text in a field or an empty string if nothing is selected. */
|
|
1457
|
+
function getFieldSelection(field) {
|
|
1458
|
+
if (isNativeField(field)) {
|
|
1459
|
+
return field.value.slice(field.selectionStart, field.selectionEnd);
|
|
1460
|
+
}
|
|
1461
|
+
const selection = field.ownerDocument.getSelection();
|
|
1462
|
+
if (selection && field.contains(selection.anchorNode)) {
|
|
1463
|
+
// The selection is inside the field
|
|
1464
|
+
return selection.toString();
|
|
1465
|
+
}
|
|
1466
|
+
return '';
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1400
1469
|
/**
|
|
1401
1470
|
* @license
|
|
1402
1471
|
* Copyright 2017 Google LLC
|
|
@@ -1757,6 +1826,7 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
1757
1826
|
this.activeAction = null;
|
|
1758
1827
|
this.editPreviewText = "";
|
|
1759
1828
|
this.errorMessage = null;
|
|
1829
|
+
this.showRetryButton = true;
|
|
1760
1830
|
this.actionFeedback = null;
|
|
1761
1831
|
this.showNoAudioWarning = false;
|
|
1762
1832
|
}
|
|
@@ -2466,10 +2536,14 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
2466
2536
|
bottom: 72px; /* Above button */
|
|
2467
2537
|
left: 50%;
|
|
2468
2538
|
transform: translateX(-50%) translateY(8px);
|
|
2539
|
+
min-width: 200px;
|
|
2469
2540
|
max-width: 280px;
|
|
2541
|
+
width: max-content;
|
|
2470
2542
|
font-size: 13px;
|
|
2471
2543
|
color: white;
|
|
2472
2544
|
white-space: normal;
|
|
2545
|
+
word-wrap: break-word;
|
|
2546
|
+
overflow-wrap: break-word;
|
|
2473
2547
|
text-align: center;
|
|
2474
2548
|
padding: 12px 16px;
|
|
2475
2549
|
border-radius: 12px;
|
|
@@ -2682,6 +2756,7 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
2682
2756
|
.error-message {
|
|
2683
2757
|
font-size: 15px;
|
|
2684
2758
|
padding: 14px 18px;
|
|
2759
|
+
min-width: 220px;
|
|
2685
2760
|
max-width: 300px;
|
|
2686
2761
|
bottom: 94px;
|
|
2687
2762
|
}
|
|
@@ -2907,9 +2982,13 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
2907
2982
|
? b `
|
|
2908
2983
|
<div class="error-message ${showError ? "visible" : ""}">
|
|
2909
2984
|
${this.errorMessage}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2985
|
+
${this.showRetryButton
|
|
2986
|
+
? b `
|
|
2987
|
+
<button class="retry-button" @click="${this.handleRetry}">
|
|
2988
|
+
Retry Connection
|
|
2989
|
+
</button>
|
|
2990
|
+
`
|
|
2991
|
+
: ""}
|
|
2913
2992
|
</div>
|
|
2914
2993
|
`
|
|
2915
2994
|
: ""}
|
|
@@ -3008,6 +3087,9 @@ __decorate([
|
|
|
3008
3087
|
__decorate([
|
|
3009
3088
|
n({ type: String })
|
|
3010
3089
|
], SpeechOSMicButton.prototype, "errorMessage", void 0);
|
|
3090
|
+
__decorate([
|
|
3091
|
+
n({ type: Boolean })
|
|
3092
|
+
], SpeechOSMicButton.prototype, "showRetryButton", void 0);
|
|
3011
3093
|
__decorate([
|
|
3012
3094
|
n({ type: String })
|
|
3013
3095
|
], SpeechOSMicButton.prototype, "actionFeedback", void 0);
|
|
@@ -6043,6 +6125,7 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
|
|
|
6043
6125
|
super(...arguments);
|
|
6044
6126
|
this.open = false;
|
|
6045
6127
|
this.text = "";
|
|
6128
|
+
this.mode = "dictation";
|
|
6046
6129
|
this.copied = false;
|
|
6047
6130
|
this.copyTimeout = null;
|
|
6048
6131
|
}
|
|
@@ -6122,6 +6205,41 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
|
|
|
6122
6205
|
color: #10b981;
|
|
6123
6206
|
flex-shrink: 0;
|
|
6124
6207
|
}
|
|
6208
|
+
|
|
6209
|
+
/* Edit mode styles */
|
|
6210
|
+
:host([mode="edit"]) .logo-icon {
|
|
6211
|
+
background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
|
|
6212
|
+
}
|
|
6213
|
+
|
|
6214
|
+
:host([mode="edit"]) .modal-title {
|
|
6215
|
+
background: linear-gradient(135deg, #a78bfa 0%, #818cf8 100%);
|
|
6216
|
+
-webkit-background-clip: text;
|
|
6217
|
+
-webkit-text-fill-color: transparent;
|
|
6218
|
+
background-clip: text;
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
:host([mode="edit"]) .hint {
|
|
6222
|
+
background: rgba(139, 92, 246, 0.08);
|
|
6223
|
+
}
|
|
6224
|
+
|
|
6225
|
+
:host([mode="edit"]) .hint-icon {
|
|
6226
|
+
color: #8b5cf6;
|
|
6227
|
+
}
|
|
6228
|
+
|
|
6229
|
+
:host([mode="edit"]) .btn-primary {
|
|
6230
|
+
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
|
6231
|
+
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6234
|
+
:host([mode="edit"]) .btn-primary:hover {
|
|
6235
|
+
background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
|
|
6236
|
+
box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
|
|
6237
|
+
}
|
|
6238
|
+
|
|
6239
|
+
:host([mode="edit"]) .btn-success {
|
|
6240
|
+
background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
|
|
6241
|
+
box-shadow: 0 4px 12px rgba(167, 139, 250, 0.3);
|
|
6242
|
+
}
|
|
6125
6243
|
`,
|
|
6126
6244
|
]; }
|
|
6127
6245
|
disconnectedCallback() {
|
|
@@ -6174,6 +6292,17 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
|
|
|
6174
6292
|
console.error("[SpeechOS] Failed to copy text:", err);
|
|
6175
6293
|
}
|
|
6176
6294
|
}
|
|
6295
|
+
get modalTitle() {
|
|
6296
|
+
return this.mode === "edit" ? "Edit Complete" : "Dictation Complete";
|
|
6297
|
+
}
|
|
6298
|
+
get modalIcon() {
|
|
6299
|
+
return this.mode === "edit" ? editIcon(18) : micIcon(18);
|
|
6300
|
+
}
|
|
6301
|
+
get hintText() {
|
|
6302
|
+
return this.mode === "edit"
|
|
6303
|
+
? "Tip: The editor didn't accept the edit. Copy and paste manually."
|
|
6304
|
+
: "Tip: Focus a text field first to auto-insert next time";
|
|
6305
|
+
}
|
|
6177
6306
|
render() {
|
|
6178
6307
|
return b `
|
|
6179
6308
|
<div
|
|
@@ -6183,8 +6312,8 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
|
|
|
6183
6312
|
<div class="modal-card">
|
|
6184
6313
|
<div class="modal-header">
|
|
6185
6314
|
<div class="header-content">
|
|
6186
|
-
<div class="logo-icon">${
|
|
6187
|
-
<h2 class="modal-title"
|
|
6315
|
+
<div class="logo-icon">${this.modalIcon}</div>
|
|
6316
|
+
<h2 class="modal-title">${this.modalTitle}</h2>
|
|
6188
6317
|
</div>
|
|
6189
6318
|
<button
|
|
6190
6319
|
class="close-button"
|
|
@@ -6201,7 +6330,7 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
|
|
|
6201
6330
|
<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">
|
|
6202
6331
|
<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
|
|
6203
6332
|
</svg>
|
|
6204
|
-
<span
|
|
6333
|
+
<span>${this.hintText}</span>
|
|
6205
6334
|
</div>
|
|
6206
6335
|
</div>
|
|
6207
6336
|
|
|
@@ -6228,6 +6357,9 @@ __decorate([
|
|
|
6228
6357
|
__decorate([
|
|
6229
6358
|
n({ type: String })
|
|
6230
6359
|
], SpeechOSDictationOutputModal.prototype, "text", void 0);
|
|
6360
|
+
__decorate([
|
|
6361
|
+
n({ type: String, reflect: true })
|
|
6362
|
+
], SpeechOSDictationOutputModal.prototype, "mode", void 0);
|
|
6231
6363
|
__decorate([
|
|
6232
6364
|
r()
|
|
6233
6365
|
], SpeechOSDictationOutputModal.prototype, "copied", void 0);
|
|
@@ -6405,9 +6537,11 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6405
6537
|
this.settingsOpenFromWarning = false;
|
|
6406
6538
|
this.dictationModalOpen = false;
|
|
6407
6539
|
this.dictationModalText = "";
|
|
6540
|
+
this.dictationModalMode = "dictation";
|
|
6408
6541
|
this.editHelpModalOpen = false;
|
|
6409
6542
|
this.actionFeedback = null;
|
|
6410
6543
|
this.showNoAudioWarning = false;
|
|
6544
|
+
this.isErrorRetryable = true;
|
|
6411
6545
|
this.dictationTargetElement = null;
|
|
6412
6546
|
this.editTargetElement = null;
|
|
6413
6547
|
this.dictationCursorStart = null;
|
|
@@ -6556,6 +6690,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6556
6690
|
this.errorEventUnsubscribe = events.on("error", (payload) => {
|
|
6557
6691
|
if (this.widgetState.recordingState !== "idle" &&
|
|
6558
6692
|
this.widgetState.recordingState !== "error") {
|
|
6693
|
+
// Check if this is a non-retryable error (e.g., CSP blocked connection)
|
|
6694
|
+
this.isErrorRetryable = payload.code !== "connection_blocked";
|
|
6559
6695
|
state.setError(payload.message);
|
|
6560
6696
|
getBackend().disconnect().catch(() => { });
|
|
6561
6697
|
}
|
|
@@ -6630,6 +6766,9 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6630
6766
|
if (changedProperties.has("dictationModalText") && this.dictationModalElement) {
|
|
6631
6767
|
this.dictationModalElement.text = this.dictationModalText;
|
|
6632
6768
|
}
|
|
6769
|
+
if (changedProperties.has("dictationModalMode") && this.dictationModalElement) {
|
|
6770
|
+
this.dictationModalElement.mode = this.dictationModalMode;
|
|
6771
|
+
}
|
|
6633
6772
|
if (changedProperties.has("editHelpModalOpen") && this.editHelpModalElement) {
|
|
6634
6773
|
this.editHelpModalElement.open = this.editHelpModalOpen;
|
|
6635
6774
|
}
|
|
@@ -6833,13 +6972,24 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6833
6972
|
// Track result for consecutive failure detection
|
|
6834
6973
|
this.trackActionResult(!!transcription);
|
|
6835
6974
|
if (transcription) {
|
|
6975
|
+
if (getConfig().debug) {
|
|
6976
|
+
console.log("[SpeechOS] Transcription received:", {
|
|
6977
|
+
transcription,
|
|
6978
|
+
dictationTargetElement: this.dictationTargetElement,
|
|
6979
|
+
tagName: this.dictationTargetElement?.tagName,
|
|
6980
|
+
});
|
|
6981
|
+
}
|
|
6836
6982
|
// Check if we have a target element to insert into
|
|
6837
6983
|
if (this.dictationTargetElement) {
|
|
6838
6984
|
this.insertTranscription(transcription);
|
|
6839
6985
|
}
|
|
6840
6986
|
else {
|
|
6841
6987
|
// No target element - show dictation output modal
|
|
6988
|
+
if (getConfig().debug) {
|
|
6989
|
+
console.log("[SpeechOS] No target element, showing dictation modal");
|
|
6990
|
+
}
|
|
6842
6991
|
this.dictationModalText = transcription;
|
|
6992
|
+
this.dictationModalMode = "dictation";
|
|
6843
6993
|
this.dictationModalOpen = true;
|
|
6844
6994
|
}
|
|
6845
6995
|
transcriptStore.saveTranscript(transcription, "dictate");
|
|
@@ -6984,41 +7134,68 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6984
7134
|
return;
|
|
6985
7135
|
}
|
|
6986
7136
|
const tagName = target.tagName.toLowerCase();
|
|
7137
|
+
const originalContent = this.getElementContent(target) || "";
|
|
6987
7138
|
if (tagName === "input" || tagName === "textarea") {
|
|
6988
7139
|
const inputEl = target;
|
|
7140
|
+
// Ensure DOM focus is on the input before inserting
|
|
7141
|
+
inputEl.focus();
|
|
7142
|
+
// Restore cursor position before inserting
|
|
6989
7143
|
const start = this.dictationCursorStart ?? inputEl.value.length;
|
|
6990
7144
|
const end = this.dictationCursorEnd ?? inputEl.value.length;
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
inputEl
|
|
6994
|
-
if (this.supportsSelection(inputEl)) {
|
|
6995
|
-
const newCursorPos = start + text.length;
|
|
6996
|
-
inputEl.setSelectionRange(newCursorPos, newCursorPos);
|
|
6997
|
-
}
|
|
6998
|
-
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
|
|
6999
|
-
inputEl.focus();
|
|
7145
|
+
inputEl.setSelectionRange(start, end);
|
|
7146
|
+
// Use text-field-edit to insert text (handles undo, events, etc.)
|
|
7147
|
+
insertTextIntoField(inputEl, text);
|
|
7000
7148
|
state.setFocusedElement(inputEl);
|
|
7001
7149
|
}
|
|
7002
7150
|
else if (target.isContentEditable) {
|
|
7003
7151
|
target.focus();
|
|
7004
7152
|
state.setFocusedElement(target);
|
|
7005
|
-
|
|
7006
|
-
target
|
|
7007
|
-
const selection = window.getSelection();
|
|
7008
|
-
if (selection) {
|
|
7009
|
-
const range = document.createRange();
|
|
7010
|
-
range.selectNodeContents(textNode);
|
|
7011
|
-
range.collapse(false);
|
|
7012
|
-
selection.removeAllRanges();
|
|
7013
|
-
selection.addRange(range);
|
|
7014
|
-
}
|
|
7015
|
-
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
7153
|
+
// Use text-field-edit for contentEditable elements
|
|
7154
|
+
insertTextIntoField(target, text);
|
|
7016
7155
|
}
|
|
7017
7156
|
events.emit("transcription:inserted", { text, element: target });
|
|
7157
|
+
// Verify insertion was applied after DOM updates
|
|
7158
|
+
this.verifyInsertionApplied(target, text, originalContent);
|
|
7018
7159
|
this.dictationTargetElement = null;
|
|
7019
7160
|
this.dictationCursorStart = null;
|
|
7020
7161
|
this.dictationCursorEnd = null;
|
|
7021
7162
|
}
|
|
7163
|
+
/**
|
|
7164
|
+
* Verify that a dictation insertion was actually applied to the target element.
|
|
7165
|
+
* Some custom editors (CodeMirror, Monaco, Slate, etc.) don't respond to
|
|
7166
|
+
* standard DOM editing methods. If the insertion fails, show a fallback modal.
|
|
7167
|
+
*/
|
|
7168
|
+
verifyInsertionApplied(target, insertedText, originalContent) {
|
|
7169
|
+
// Use requestAnimationFrame to check after DOM updates
|
|
7170
|
+
requestAnimationFrame(() => {
|
|
7171
|
+
const tagName = target.tagName.toLowerCase();
|
|
7172
|
+
let currentContent = "";
|
|
7173
|
+
if (tagName === "input" || tagName === "textarea") {
|
|
7174
|
+
currentContent = target.value;
|
|
7175
|
+
}
|
|
7176
|
+
else if (target.isContentEditable) {
|
|
7177
|
+
currentContent = target.textContent || "";
|
|
7178
|
+
}
|
|
7179
|
+
// Check if the insertion was applied:
|
|
7180
|
+
// - Content should contain the inserted text
|
|
7181
|
+
// - Or content should be different from original (for empty fields)
|
|
7182
|
+
const insertionApplied = currentContent.includes(insertedText) ||
|
|
7183
|
+
(originalContent === "" && currentContent !== "");
|
|
7184
|
+
if (!insertionApplied) {
|
|
7185
|
+
if (getConfig().debug) {
|
|
7186
|
+
console.log("[SpeechOS] Dictation failed to insert, showing fallback modal", {
|
|
7187
|
+
insertedText,
|
|
7188
|
+
currentContent,
|
|
7189
|
+
originalContent,
|
|
7190
|
+
});
|
|
7191
|
+
}
|
|
7192
|
+
// Show fallback modal with dictation mode styling
|
|
7193
|
+
this.dictationModalText = insertedText;
|
|
7194
|
+
this.dictationModalMode = "dictation";
|
|
7195
|
+
this.dictationModalOpen = true;
|
|
7196
|
+
}
|
|
7197
|
+
});
|
|
7198
|
+
}
|
|
7022
7199
|
handleActionSelect(event) {
|
|
7023
7200
|
const { action } = event.detail;
|
|
7024
7201
|
// Clear any existing command feedback when a new action is selected
|
|
@@ -7057,6 +7234,13 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7057
7234
|
this.dictationTargetElement = this.widgetState.focusedElement;
|
|
7058
7235
|
this.dictationCursorStart = null;
|
|
7059
7236
|
this.dictationCursorEnd = null;
|
|
7237
|
+
if (getConfig().debug) {
|
|
7238
|
+
console.log("[SpeechOS] startDictation:", {
|
|
7239
|
+
focusedElement: this.widgetState.focusedElement,
|
|
7240
|
+
dictationTargetElement: this.dictationTargetElement,
|
|
7241
|
+
tagName: this.dictationTargetElement?.tagName,
|
|
7242
|
+
});
|
|
7243
|
+
}
|
|
7060
7244
|
if (this.dictationTargetElement) {
|
|
7061
7245
|
const tagName = this.dictationTargetElement.tagName.toLowerCase();
|
|
7062
7246
|
if (tagName === "input" || tagName === "textarea") {
|
|
@@ -7082,15 +7266,18 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7082
7266
|
// Ensure minimum animation duration before transitioning to recording
|
|
7083
7267
|
const elapsed = Date.now() - connectingStartTime;
|
|
7084
7268
|
const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
|
|
7269
|
+
const startRecording = () => {
|
|
7270
|
+
if (state.getState().recordingState === "error") {
|
|
7271
|
+
return;
|
|
7272
|
+
}
|
|
7273
|
+
state.setRecordingState("recording");
|
|
7274
|
+
this.startNoAudioWarningTracking();
|
|
7275
|
+
};
|
|
7085
7276
|
if (remainingDelay > 0) {
|
|
7086
|
-
setTimeout(
|
|
7087
|
-
state.setRecordingState("recording");
|
|
7088
|
-
this.startNoAudioWarningTracking();
|
|
7089
|
-
}, remainingDelay);
|
|
7277
|
+
setTimeout(startRecording, remainingDelay);
|
|
7090
7278
|
}
|
|
7091
7279
|
else {
|
|
7092
|
-
|
|
7093
|
-
this.startNoAudioWarningTracking();
|
|
7280
|
+
startRecording();
|
|
7094
7281
|
}
|
|
7095
7282
|
},
|
|
7096
7283
|
});
|
|
@@ -7111,6 +7298,13 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7111
7298
|
this.editSelectionStart = null;
|
|
7112
7299
|
this.editSelectionEnd = null;
|
|
7113
7300
|
this.editSelectedText = "";
|
|
7301
|
+
if (getConfig().debug) {
|
|
7302
|
+
console.log("[SpeechOS] startEdit:", {
|
|
7303
|
+
focusedElement: this.widgetState.focusedElement,
|
|
7304
|
+
editTargetElement: this.editTargetElement,
|
|
7305
|
+
tagName: this.editTargetElement?.tagName,
|
|
7306
|
+
});
|
|
7307
|
+
}
|
|
7114
7308
|
if (this.editTargetElement) {
|
|
7115
7309
|
const tagName = this.editTargetElement.tagName.toLowerCase();
|
|
7116
7310
|
if (tagName === "input" || tagName === "textarea") {
|
|
@@ -7121,7 +7315,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7121
7315
|
const start = this.editSelectionStart ?? 0;
|
|
7122
7316
|
const end = this.editSelectionEnd ?? 0;
|
|
7123
7317
|
if (start !== end) {
|
|
7124
|
-
|
|
7318
|
+
// Use getFieldSelection from text-field-edit
|
|
7319
|
+
this.editSelectedText = getFieldSelection(inputEl);
|
|
7125
7320
|
}
|
|
7126
7321
|
}
|
|
7127
7322
|
else {
|
|
@@ -7130,13 +7325,11 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7130
7325
|
}
|
|
7131
7326
|
}
|
|
7132
7327
|
else if (this.editTargetElement.isContentEditable) {
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
this.editSelectedText = selectedText;
|
|
7139
|
-
}
|
|
7328
|
+
// Use getFieldSelection from text-field-edit for contentEditable too
|
|
7329
|
+
const selectedText = getFieldSelection(this.editTargetElement);
|
|
7330
|
+
this.editSelectionStart = 0;
|
|
7331
|
+
this.editSelectionEnd = selectedText.length;
|
|
7332
|
+
this.editSelectedText = selectedText;
|
|
7140
7333
|
}
|
|
7141
7334
|
}
|
|
7142
7335
|
// Capture the content to edit at start time (sent with auth message)
|
|
@@ -7153,15 +7346,18 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7153
7346
|
// Ensure minimum animation duration before transitioning to recording
|
|
7154
7347
|
const elapsed = Date.now() - connectingStartTime;
|
|
7155
7348
|
const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
|
|
7349
|
+
const startRecording = () => {
|
|
7350
|
+
if (state.getState().recordingState === "error") {
|
|
7351
|
+
return;
|
|
7352
|
+
}
|
|
7353
|
+
state.setRecordingState("recording");
|
|
7354
|
+
this.startNoAudioWarningTracking();
|
|
7355
|
+
};
|
|
7156
7356
|
if (remainingDelay > 0) {
|
|
7157
|
-
setTimeout(
|
|
7158
|
-
state.setRecordingState("recording");
|
|
7159
|
-
this.startNoAudioWarningTracking();
|
|
7160
|
-
}, remainingDelay);
|
|
7357
|
+
setTimeout(startRecording, remainingDelay);
|
|
7161
7358
|
}
|
|
7162
7359
|
else {
|
|
7163
|
-
|
|
7164
|
-
this.startNoAudioWarningTracking();
|
|
7360
|
+
startRecording();
|
|
7165
7361
|
}
|
|
7166
7362
|
},
|
|
7167
7363
|
});
|
|
@@ -7229,15 +7425,18 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7229
7425
|
// Ensure minimum animation duration before transitioning to recording
|
|
7230
7426
|
const elapsed = Date.now() - connectingStartTime;
|
|
7231
7427
|
const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
|
|
7428
|
+
const startRecording = () => {
|
|
7429
|
+
if (state.getState().recordingState === "error") {
|
|
7430
|
+
return;
|
|
7431
|
+
}
|
|
7432
|
+
state.setRecordingState("recording");
|
|
7433
|
+
this.startNoAudioWarningTracking();
|
|
7434
|
+
};
|
|
7232
7435
|
if (remainingDelay > 0) {
|
|
7233
|
-
setTimeout(
|
|
7234
|
-
state.setRecordingState("recording");
|
|
7235
|
-
this.startNoAudioWarningTracking();
|
|
7236
|
-
}, remainingDelay);
|
|
7436
|
+
setTimeout(startRecording, remainingDelay);
|
|
7237
7437
|
}
|
|
7238
7438
|
else {
|
|
7239
|
-
|
|
7240
|
-
this.startNoAudioWarningTracking();
|
|
7439
|
+
startRecording();
|
|
7241
7440
|
}
|
|
7242
7441
|
},
|
|
7243
7442
|
});
|
|
@@ -7276,8 +7475,6 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7276
7475
|
// Note: command:complete event is already emitted by the backend
|
|
7277
7476
|
// when the command_result message is received, so we don't emit here
|
|
7278
7477
|
state.completeRecording();
|
|
7279
|
-
// Keep widget visible but collapsed (just mic button, no action bubbles)
|
|
7280
|
-
state.setState({ isExpanded: false });
|
|
7281
7478
|
// Show command feedback
|
|
7282
7479
|
this.showActionFeedback(result ? "command-success" : "command-none");
|
|
7283
7480
|
backend.disconnect().catch(() => { });
|
|
@@ -7413,21 +7610,14 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7413
7610
|
const tagName = element.tagName.toLowerCase();
|
|
7414
7611
|
if (tagName === "input" || tagName === "textarea") {
|
|
7415
7612
|
const inputEl = element;
|
|
7416
|
-
const
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
const hasSelection = start !== end;
|
|
7420
|
-
if (hasSelection) {
|
|
7421
|
-
return fullContent.substring(start, end);
|
|
7422
|
-
}
|
|
7423
|
-
return fullContent;
|
|
7613
|
+
const selectedText = getFieldSelection(inputEl);
|
|
7614
|
+
// If there's selected text, return it; otherwise return full content
|
|
7615
|
+
return selectedText || inputEl.value;
|
|
7424
7616
|
}
|
|
7425
7617
|
else if (element.isContentEditable) {
|
|
7426
|
-
const
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
}
|
|
7430
|
-
return element.textContent || "";
|
|
7618
|
+
const selectedText = getFieldSelection(element);
|
|
7619
|
+
// If there's selected text, return it; otherwise return full content
|
|
7620
|
+
return selectedText || element.textContent || "";
|
|
7431
7621
|
}
|
|
7432
7622
|
return "";
|
|
7433
7623
|
}
|
|
@@ -7442,40 +7632,46 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7442
7632
|
if (tagName === "input" || tagName === "textarea") {
|
|
7443
7633
|
const inputEl = target;
|
|
7444
7634
|
originalContent = inputEl.value;
|
|
7635
|
+
// Ensure DOM focus is on the input before editing
|
|
7445
7636
|
inputEl.focus();
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
inputEl.setSelectionRange(0, inputEl.value.length);
|
|
7455
|
-
}
|
|
7456
|
-
document.execCommand("insertText", false, editedText);
|
|
7637
|
+
// Restore the original selection/cursor position
|
|
7638
|
+
const selectionStart = this.editSelectionStart ?? 0;
|
|
7639
|
+
const selectionEnd = this.editSelectionEnd ?? inputEl.value.length;
|
|
7640
|
+
const hasSelection = selectionStart !== selectionEnd;
|
|
7641
|
+
if (hasSelection) {
|
|
7642
|
+
// Restore selection, then use insertTextIntoField() to replace it
|
|
7643
|
+
inputEl.setSelectionRange(selectionStart, selectionEnd);
|
|
7644
|
+
insertTextIntoField(inputEl, editedText);
|
|
7457
7645
|
}
|
|
7458
7646
|
else {
|
|
7459
|
-
|
|
7460
|
-
inputEl
|
|
7647
|
+
// No selection - replace entire content using setFieldText()
|
|
7648
|
+
setFieldText(inputEl, editedText);
|
|
7461
7649
|
}
|
|
7462
7650
|
state.setFocusedElement(inputEl);
|
|
7463
7651
|
}
|
|
7464
7652
|
else if (target.isContentEditable) {
|
|
7465
7653
|
originalContent = target.textContent || "";
|
|
7466
|
-
target.focus();
|
|
7467
|
-
state.setFocusedElement(target);
|
|
7468
7654
|
const hasSelection = this.editSelectionStart !== null &&
|
|
7469
7655
|
this.editSelectionEnd !== null &&
|
|
7470
7656
|
this.editSelectionStart !== this.editSelectionEnd;
|
|
7471
|
-
if (
|
|
7657
|
+
if (hasSelection) {
|
|
7658
|
+
// Selection exists - focus and insert (assumes selection is still active or we restore it)
|
|
7659
|
+
target.focus();
|
|
7660
|
+
insertTextIntoField(target, editedText);
|
|
7661
|
+
}
|
|
7662
|
+
else {
|
|
7663
|
+
// No selection - select all content first, then replace with insertTextIntoField()
|
|
7664
|
+
target.focus();
|
|
7472
7665
|
const selection = window.getSelection();
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7666
|
+
if (selection) {
|
|
7667
|
+
const range = document.createRange();
|
|
7668
|
+
range.selectNodeContents(target);
|
|
7669
|
+
selection.removeAllRanges();
|
|
7670
|
+
selection.addRange(range);
|
|
7671
|
+
}
|
|
7672
|
+
insertTextIntoField(target, editedText);
|
|
7477
7673
|
}
|
|
7478
|
-
|
|
7674
|
+
state.setFocusedElement(target);
|
|
7479
7675
|
}
|
|
7480
7676
|
transcriptStore.saveTranscript(editedText, "edit", originalContent);
|
|
7481
7677
|
events.emit("edit:applied", {
|
|
@@ -7484,11 +7680,54 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7484
7680
|
element: target,
|
|
7485
7681
|
});
|
|
7486
7682
|
state.completeRecording();
|
|
7683
|
+
// Verify edit was applied after DOM updates
|
|
7684
|
+
this.verifyEditApplied(target, editedText, originalContent);
|
|
7487
7685
|
this.editTargetElement = null;
|
|
7488
7686
|
this.editSelectionStart = null;
|
|
7489
7687
|
this.editSelectionEnd = null;
|
|
7490
7688
|
this.editSelectedText = "";
|
|
7491
7689
|
}
|
|
7690
|
+
/**
|
|
7691
|
+
* Verify that an edit was actually applied to the target element.
|
|
7692
|
+
* Some custom editors (CodeMirror, Monaco, Slate, etc.) don't respond to
|
|
7693
|
+
* standard DOM editing methods. If the edit fails, show a fallback modal.
|
|
7694
|
+
*/
|
|
7695
|
+
verifyEditApplied(target, editedText, originalContent) {
|
|
7696
|
+
// Use requestAnimationFrame to check after DOM updates
|
|
7697
|
+
requestAnimationFrame(() => {
|
|
7698
|
+
const tagName = target.tagName.toLowerCase();
|
|
7699
|
+
let currentContent = "";
|
|
7700
|
+
if (tagName === "input" || tagName === "textarea") {
|
|
7701
|
+
currentContent = target.value;
|
|
7702
|
+
}
|
|
7703
|
+
else if (target.isContentEditable) {
|
|
7704
|
+
currentContent = target.textContent || "";
|
|
7705
|
+
}
|
|
7706
|
+
// Normalize whitespace for comparison
|
|
7707
|
+
const normalizedCurrent = currentContent.trim();
|
|
7708
|
+
const normalizedEdited = editedText.trim();
|
|
7709
|
+
const normalizedOriginal = originalContent.trim();
|
|
7710
|
+
// Check if the edit was applied:
|
|
7711
|
+
// - Content should be different from original (unless edit was no-op)
|
|
7712
|
+
// - Content should contain or match the edited text
|
|
7713
|
+
const editApplied = normalizedCurrent !== normalizedOriginal ||
|
|
7714
|
+
normalizedCurrent === normalizedEdited ||
|
|
7715
|
+
normalizedCurrent.includes(normalizedEdited);
|
|
7716
|
+
if (!editApplied) {
|
|
7717
|
+
if (getConfig().debug) {
|
|
7718
|
+
console.log("[SpeechOS] Edit failed to apply, showing fallback modal", {
|
|
7719
|
+
expected: editedText,
|
|
7720
|
+
actual: currentContent,
|
|
7721
|
+
original: originalContent,
|
|
7722
|
+
});
|
|
7723
|
+
}
|
|
7724
|
+
// Show fallback modal with edit mode styling
|
|
7725
|
+
this.dictationModalText = editedText;
|
|
7726
|
+
this.dictationModalMode = "edit";
|
|
7727
|
+
this.dictationModalOpen = true;
|
|
7728
|
+
}
|
|
7729
|
+
});
|
|
7730
|
+
}
|
|
7492
7731
|
render() {
|
|
7493
7732
|
if (!this.widgetState.isVisible) {
|
|
7494
7733
|
this.setAttribute("hidden", "");
|
|
@@ -7516,6 +7755,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7516
7755
|
activeAction="${this.widgetState.activeAction || ""}"
|
|
7517
7756
|
editPreviewText="${this.editSelectedText}"
|
|
7518
7757
|
errorMessage="${this.widgetState.errorMessage || ""}"
|
|
7758
|
+
?showRetryButton="${this.isErrorRetryable}"
|
|
7519
7759
|
.actionFeedback="${this.actionFeedback}"
|
|
7520
7760
|
?showNoAudioWarning="${this.showNoAudioWarning}"
|
|
7521
7761
|
@mic-click="${this.handleMicClick}"
|
|
@@ -7542,6 +7782,9 @@ __decorate([
|
|
|
7542
7782
|
__decorate([
|
|
7543
7783
|
r()
|
|
7544
7784
|
], SpeechOSWidget.prototype, "dictationModalText", void 0);
|
|
7785
|
+
__decorate([
|
|
7786
|
+
r()
|
|
7787
|
+
], SpeechOSWidget.prototype, "dictationModalMode", void 0);
|
|
7545
7788
|
__decorate([
|
|
7546
7789
|
r()
|
|
7547
7790
|
], SpeechOSWidget.prototype, "editHelpModalOpen", void 0);
|
|
@@ -7551,6 +7794,9 @@ __decorate([
|
|
|
7551
7794
|
__decorate([
|
|
7552
7795
|
r()
|
|
7553
7796
|
], SpeechOSWidget.prototype, "showNoAudioWarning", void 0);
|
|
7797
|
+
__decorate([
|
|
7798
|
+
r()
|
|
7799
|
+
], SpeechOSWidget.prototype, "isErrorRetryable", void 0);
|
|
7554
7800
|
SpeechOSWidget = SpeechOSWidget_1 = __decorate([
|
|
7555
7801
|
t$1("speechos-widget")
|
|
7556
7802
|
], SpeechOSWidget);
|