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