@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.iife.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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:
|
|
28417
|
-
message:
|
|
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 ===
|
|
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 !==
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
31666
|
-
|
|
31667
|
-
|
|
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">${
|
|
34942
|
-
<h2 class="modal-title"
|
|
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
|
|
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
|
-
|
|
35741
|
-
|
|
35742
|
-
inputEl
|
|
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
|
-
|
|
35755
|
-
target
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35883
|
-
|
|
35884
|
-
|
|
35885
|
-
|
|
35886
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
36166
|
-
|
|
36167
|
-
|
|
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
|
|
36176
|
-
|
|
36177
|
-
|
|
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
|
-
|
|
36195
|
-
|
|
36196
|
-
|
|
36197
|
-
|
|
36198
|
-
|
|
36199
|
-
|
|
36200
|
-
|
|
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
|
-
|
|
36209
|
-
inputEl
|
|
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 (
|
|
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
|
-
|
|
36223
|
-
|
|
36224
|
-
|
|
36225
|
-
|
|
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
|
-
|
|
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);
|