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