@speechos/client 0.2.5 → 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 +645 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.iife.js +670 -134
- package/dist/index.iife.js.map +1 -1
- package/dist/index.iife.min.js +248 -107
- package/dist/index.iife.min.js.map +1 -1
- package/dist/index.js +646 -123
- 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 +5 -2
- package/dist/ui/mic-button.d.ts.map +1 -1
- package/dist/ui/mic-button.test.d.ts +1 -1
- package/dist/ui/widget.d.ts +41 -4
- 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
|
}
|
|
@@ -28518,6 +28527,10 @@
|
|
|
28518
28527
|
}
|
|
28519
28528
|
handleIntermediateTranscription(message) {
|
|
28520
28529
|
const config = getConfig();
|
|
28530
|
+
events.emit("transcription:interim", {
|
|
28531
|
+
transcript: message.transcript,
|
|
28532
|
+
isFinal: message.is_final
|
|
28533
|
+
});
|
|
28521
28534
|
if (config.debug) console.log("[SpeechOS] Intermediate transcription:", message.transcript, "final:", message.is_final);
|
|
28522
28535
|
}
|
|
28523
28536
|
handleFinalTranscript(message) {
|
|
@@ -28654,7 +28667,7 @@
|
|
|
28654
28667
|
* the transcript. Uses the same pattern as LiveKit's ReadableStream approach.
|
|
28655
28668
|
*/
|
|
28656
28669
|
async waitForBufferDrain() {
|
|
28657
|
-
if (!this.ws || this.ws.readyState !==
|
|
28670
|
+
if (!this.ws || this.ws.readyState !== WS_OPEN) return;
|
|
28658
28671
|
const config = getConfig();
|
|
28659
28672
|
const startTime = Date.now();
|
|
28660
28673
|
while (this.ws.bufferedAmount > 0) {
|
|
@@ -28670,7 +28683,7 @@
|
|
|
28670
28683
|
* Send a JSON message over the WebSocket.
|
|
28671
28684
|
*/
|
|
28672
28685
|
sendMessage(message) {
|
|
28673
|
-
if (this.ws && this.ws.readyState ===
|
|
28686
|
+
if (this.ws && this.ws.readyState === WS_OPEN) this.ws.send(JSON.stringify(message));
|
|
28674
28687
|
}
|
|
28675
28688
|
/**
|
|
28676
28689
|
* Disconnect from the WebSocket.
|
|
@@ -28712,7 +28725,7 @@
|
|
|
28712
28725
|
* Check if connected to WebSocket.
|
|
28713
28726
|
*/
|
|
28714
28727
|
isConnected() {
|
|
28715
|
-
return this.ws !== null && this.ws.readyState ===
|
|
28728
|
+
return this.ws !== null && this.ws.readyState === WS_OPEN;
|
|
28716
28729
|
}
|
|
28717
28730
|
/**
|
|
28718
28731
|
* Get the last input text from a command result.
|
|
@@ -28893,6 +28906,10 @@
|
|
|
28893
28906
|
this.focusHandler = (event) => {
|
|
28894
28907
|
const target = event.target;
|
|
28895
28908
|
if (isFormField(target)) {
|
|
28909
|
+
console.log("[SpeechOS] FormDetector: focus on form field", {
|
|
28910
|
+
element: target,
|
|
28911
|
+
tagName: target?.tagName,
|
|
28912
|
+
});
|
|
28896
28913
|
state.setFocusedElement(target);
|
|
28897
28914
|
state.show();
|
|
28898
28915
|
events.emit("form:focus", { element: target });
|
|
@@ -30148,6 +30165,71 @@
|
|
|
30148
30165
|
deleteTranscript: deleteTranscript,
|
|
30149
30166
|
};
|
|
30150
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
|
+
|
|
30151
30233
|
/**
|
|
30152
30234
|
* @license
|
|
30153
30235
|
* Copyright 2017 Google LLC
|
|
@@ -30508,7 +30590,9 @@
|
|
|
30508
30590
|
this.activeAction = null;
|
|
30509
30591
|
this.editPreviewText = "";
|
|
30510
30592
|
this.errorMessage = null;
|
|
30511
|
-
this.
|
|
30593
|
+
this.showRetryButton = true;
|
|
30594
|
+
this.actionFeedback = null;
|
|
30595
|
+
this.showNoAudioWarning = false;
|
|
30512
30596
|
}
|
|
30513
30597
|
static { this.styles = [
|
|
30514
30598
|
themeStyles,
|
|
@@ -31098,8 +31182,9 @@
|
|
|
31098
31182
|
background-position: center;
|
|
31099
31183
|
}
|
|
31100
31184
|
|
|
31101
|
-
/* Command feedback badge - no match state (neutral gray) */
|
|
31102
|
-
.status-label.command-none
|
|
31185
|
+
/* Command/edit feedback badge - no match/empty state (neutral gray) */
|
|
31186
|
+
.status-label.command-none,
|
|
31187
|
+
.status-label.edit-empty {
|
|
31103
31188
|
background: #4b5563;
|
|
31104
31189
|
box-shadow: 0 4px 12px rgba(75, 85, 99, 0.3);
|
|
31105
31190
|
animation: command-feedback-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)
|
|
@@ -31215,10 +31300,14 @@
|
|
|
31215
31300
|
bottom: 72px; /* Above button */
|
|
31216
31301
|
left: 50%;
|
|
31217
31302
|
transform: translateX(-50%) translateY(8px);
|
|
31303
|
+
min-width: 200px;
|
|
31218
31304
|
max-width: 280px;
|
|
31305
|
+
width: max-content;
|
|
31219
31306
|
font-size: 13px;
|
|
31220
31307
|
color: white;
|
|
31221
31308
|
white-space: normal;
|
|
31309
|
+
word-wrap: break-word;
|
|
31310
|
+
overflow-wrap: break-word;
|
|
31222
31311
|
text-align: center;
|
|
31223
31312
|
padding: 12px 16px;
|
|
31224
31313
|
border-radius: 12px;
|
|
@@ -31256,6 +31345,60 @@
|
|
|
31256
31345
|
border-color: rgba(255, 255, 255, 0.5);
|
|
31257
31346
|
}
|
|
31258
31347
|
|
|
31348
|
+
/* No audio warning banner */
|
|
31349
|
+
.no-audio-warning {
|
|
31350
|
+
position: absolute;
|
|
31351
|
+
bottom: 120px; /* Above button and waveform visualizer */
|
|
31352
|
+
left: 50%;
|
|
31353
|
+
transform: translateX(-50%) translateY(8px);
|
|
31354
|
+
display: flex;
|
|
31355
|
+
align-items: center;
|
|
31356
|
+
gap: 8px;
|
|
31357
|
+
padding: 10px 14px;
|
|
31358
|
+
border-radius: 12px;
|
|
31359
|
+
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
|
31360
|
+
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
|
31361
|
+
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
31362
|
+
pointer-events: none;
|
|
31363
|
+
opacity: 0;
|
|
31364
|
+
white-space: nowrap;
|
|
31365
|
+
}
|
|
31366
|
+
|
|
31367
|
+
.no-audio-warning.visible {
|
|
31368
|
+
opacity: 1;
|
|
31369
|
+
transform: translateX(-50%) translateY(0);
|
|
31370
|
+
pointer-events: auto;
|
|
31371
|
+
}
|
|
31372
|
+
|
|
31373
|
+
.no-audio-warning .warning-icon {
|
|
31374
|
+
flex-shrink: 0;
|
|
31375
|
+
color: white;
|
|
31376
|
+
}
|
|
31377
|
+
|
|
31378
|
+
.no-audio-warning .warning-text {
|
|
31379
|
+
font-size: 13px;
|
|
31380
|
+
font-weight: 500;
|
|
31381
|
+
color: white;
|
|
31382
|
+
}
|
|
31383
|
+
|
|
31384
|
+
.no-audio-warning .settings-link {
|
|
31385
|
+
background: rgba(255, 255, 255, 0.2);
|
|
31386
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
31387
|
+
border-radius: 6px;
|
|
31388
|
+
padding: 4px 10px;
|
|
31389
|
+
font-size: 12px;
|
|
31390
|
+
font-weight: 600;
|
|
31391
|
+
color: white;
|
|
31392
|
+
cursor: pointer;
|
|
31393
|
+
transition: all 0.15s;
|
|
31394
|
+
white-space: nowrap;
|
|
31395
|
+
}
|
|
31396
|
+
|
|
31397
|
+
.no-audio-warning .settings-link:hover {
|
|
31398
|
+
background: rgba(255, 255, 255, 0.3);
|
|
31399
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
31400
|
+
}
|
|
31401
|
+
|
|
31259
31402
|
/* Mobile styles - 30% larger */
|
|
31260
31403
|
@media (max-width: 768px) and (hover: none) {
|
|
31261
31404
|
.mic-button {
|
|
@@ -31377,6 +31520,7 @@
|
|
|
31377
31520
|
.error-message {
|
|
31378
31521
|
font-size: 15px;
|
|
31379
31522
|
padding: 14px 18px;
|
|
31523
|
+
min-width: 220px;
|
|
31380
31524
|
max-width: 300px;
|
|
31381
31525
|
bottom: 94px;
|
|
31382
31526
|
}
|
|
@@ -31385,6 +31529,21 @@
|
|
|
31385
31529
|
padding: 8px 14px;
|
|
31386
31530
|
font-size: 14px;
|
|
31387
31531
|
}
|
|
31532
|
+
|
|
31533
|
+
.no-audio-warning {
|
|
31534
|
+
padding: 12px 16px;
|
|
31535
|
+
gap: 10px;
|
|
31536
|
+
bottom: 145px; /* Above button and waveform on mobile */
|
|
31537
|
+
}
|
|
31538
|
+
|
|
31539
|
+
.no-audio-warning .warning-text {
|
|
31540
|
+
font-size: 15px;
|
|
31541
|
+
}
|
|
31542
|
+
|
|
31543
|
+
.no-audio-warning .settings-link {
|
|
31544
|
+
padding: 6px 12px;
|
|
31545
|
+
font-size: 14px;
|
|
31546
|
+
}
|
|
31388
31547
|
}
|
|
31389
31548
|
`,
|
|
31390
31549
|
]; }
|
|
@@ -31442,6 +31601,14 @@
|
|
|
31442
31601
|
composed: true,
|
|
31443
31602
|
}));
|
|
31444
31603
|
}
|
|
31604
|
+
handleOpenSettings(e) {
|
|
31605
|
+
e.stopPropagation();
|
|
31606
|
+
e.preventDefault();
|
|
31607
|
+
this.dispatchEvent(new CustomEvent("open-settings", {
|
|
31608
|
+
bubbles: true,
|
|
31609
|
+
composed: true,
|
|
31610
|
+
}));
|
|
31611
|
+
}
|
|
31445
31612
|
getButtonClass() {
|
|
31446
31613
|
const classes = ["mic-button"];
|
|
31447
31614
|
if (this.expanded && this.recordingState === "idle") {
|
|
@@ -31526,13 +31693,16 @@
|
|
|
31526
31693
|
}
|
|
31527
31694
|
return this.recordingState;
|
|
31528
31695
|
}
|
|
31529
|
-
|
|
31530
|
-
if (this.
|
|
31696
|
+
getActionFeedbackLabel() {
|
|
31697
|
+
if (this.actionFeedback === "command-success") {
|
|
31531
31698
|
return "Got it!";
|
|
31532
31699
|
}
|
|
31533
|
-
if (this.
|
|
31700
|
+
if (this.actionFeedback === "command-none") {
|
|
31534
31701
|
return "No command matched";
|
|
31535
31702
|
}
|
|
31703
|
+
if (this.actionFeedback === "edit-empty") {
|
|
31704
|
+
return "Couldn't understand edit";
|
|
31705
|
+
}
|
|
31536
31706
|
return "";
|
|
31537
31707
|
}
|
|
31538
31708
|
render() {
|
|
@@ -31542,9 +31712,9 @@
|
|
|
31542
31712
|
const showSiriEdit = this.recordingState === "processing" && this.activeAction === "edit";
|
|
31543
31713
|
const statusLabel = this.getStatusLabel();
|
|
31544
31714
|
const showVisualizer = this.shouldShowVisualizer();
|
|
31545
|
-
// Show status label during recording (either visualizer or edit text) OR
|
|
31546
|
-
const
|
|
31547
|
-
const showStatus = this.recordingState === "recording" ||
|
|
31715
|
+
// Show status label during recording (either visualizer or edit text) OR action feedback
|
|
31716
|
+
const showActionFeedback = this.recordingState === "idle" && this.actionFeedback !== null;
|
|
31717
|
+
const showStatus = this.recordingState === "recording" || showActionFeedback;
|
|
31548
31718
|
const showCancel = this.recordingState === "connecting" ||
|
|
31549
31719
|
this.recordingState === "recording" ||
|
|
31550
31720
|
this.recordingState === "processing";
|
|
@@ -31576,13 +31746,46 @@
|
|
|
31576
31746
|
? b `
|
|
31577
31747
|
<div class="error-message ${showError ? "visible" : ""}">
|
|
31578
31748
|
${this.errorMessage}
|
|
31579
|
-
|
|
31580
|
-
|
|
31581
|
-
|
|
31749
|
+
${this.showRetryButton
|
|
31750
|
+
? b `
|
|
31751
|
+
<button class="retry-button" @click="${this.handleRetry}">
|
|
31752
|
+
Retry Connection
|
|
31753
|
+
</button>
|
|
31754
|
+
`
|
|
31755
|
+
: ""}
|
|
31582
31756
|
</div>
|
|
31583
31757
|
`
|
|
31584
31758
|
: ""}
|
|
31585
31759
|
|
|
31760
|
+
<div
|
|
31761
|
+
class="no-audio-warning ${this.showNoAudioWarning &&
|
|
31762
|
+
this.recordingState === "recording"
|
|
31763
|
+
? "visible"
|
|
31764
|
+
: ""}"
|
|
31765
|
+
>
|
|
31766
|
+
<svg
|
|
31767
|
+
class="warning-icon"
|
|
31768
|
+
width="16"
|
|
31769
|
+
height="16"
|
|
31770
|
+
viewBox="0 0 24 24"
|
|
31771
|
+
fill="none"
|
|
31772
|
+
stroke="currentColor"
|
|
31773
|
+
stroke-width="2"
|
|
31774
|
+
stroke-linecap="round"
|
|
31775
|
+
stroke-linejoin="round"
|
|
31776
|
+
>
|
|
31777
|
+
<path
|
|
31778
|
+
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
|
31779
|
+
/>
|
|
31780
|
+
<line x1="12" y1="9" x2="12" y2="13" />
|
|
31781
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
31782
|
+
</svg>
|
|
31783
|
+
<span class="warning-text">We're not hearing anything</span>
|
|
31784
|
+
<button class="settings-link" @click="${this.handleOpenSettings}">
|
|
31785
|
+
Check Settings
|
|
31786
|
+
</button>
|
|
31787
|
+
</div>
|
|
31788
|
+
|
|
31586
31789
|
<button
|
|
31587
31790
|
class="${this.getButtonClass()}"
|
|
31588
31791
|
@click="${this.handleClick}"
|
|
@@ -31595,14 +31798,14 @@
|
|
|
31595
31798
|
</button>
|
|
31596
31799
|
|
|
31597
31800
|
<span
|
|
31598
|
-
class="status-label ${showStatus ? "visible" : ""} ${
|
|
31599
|
-
?
|
|
31801
|
+
class="status-label ${showStatus ? "visible" : ""} ${showActionFeedback
|
|
31802
|
+
? this.actionFeedback
|
|
31600
31803
|
: showVisualizer
|
|
31601
31804
|
? "visualizer"
|
|
31602
31805
|
: this.getStatusClass()}"
|
|
31603
31806
|
>
|
|
31604
|
-
${
|
|
31605
|
-
? this.
|
|
31807
|
+
${showActionFeedback
|
|
31808
|
+
? this.getActionFeedbackLabel()
|
|
31606
31809
|
: showVisualizer
|
|
31607
31810
|
? b `<speechos-audio-visualizer
|
|
31608
31811
|
?active="${showVisualizer}"
|
|
@@ -31648,9 +31851,15 @@
|
|
|
31648
31851
|
__decorate([
|
|
31649
31852
|
n({ type: String })
|
|
31650
31853
|
], SpeechOSMicButton.prototype, "errorMessage", void 0);
|
|
31854
|
+
__decorate([
|
|
31855
|
+
n({ type: Boolean })
|
|
31856
|
+
], SpeechOSMicButton.prototype, "showRetryButton", void 0);
|
|
31651
31857
|
__decorate([
|
|
31652
31858
|
n({ type: String })
|
|
31653
|
-
], SpeechOSMicButton.prototype, "
|
|
31859
|
+
], SpeechOSMicButton.prototype, "actionFeedback", void 0);
|
|
31860
|
+
__decorate([
|
|
31861
|
+
n({ type: Boolean })
|
|
31862
|
+
], SpeechOSMicButton.prototype, "showNoAudioWarning", void 0);
|
|
31654
31863
|
SpeechOSMicButton = __decorate([
|
|
31655
31864
|
t$1("speechos-mic-button")
|
|
31656
31865
|
], SpeechOSMicButton);
|
|
@@ -34680,6 +34889,7 @@
|
|
|
34680
34889
|
super(...arguments);
|
|
34681
34890
|
this.open = false;
|
|
34682
34891
|
this.text = "";
|
|
34892
|
+
this.mode = "dictation";
|
|
34683
34893
|
this.copied = false;
|
|
34684
34894
|
this.copyTimeout = null;
|
|
34685
34895
|
}
|
|
@@ -34759,6 +34969,41 @@
|
|
|
34759
34969
|
color: #10b981;
|
|
34760
34970
|
flex-shrink: 0;
|
|
34761
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
|
+
}
|
|
34762
35007
|
`,
|
|
34763
35008
|
]; }
|
|
34764
35009
|
disconnectedCallback() {
|
|
@@ -34811,6 +35056,17 @@
|
|
|
34811
35056
|
console.error("[SpeechOS] Failed to copy text:", err);
|
|
34812
35057
|
}
|
|
34813
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
|
+
}
|
|
34814
35070
|
render() {
|
|
34815
35071
|
return b `
|
|
34816
35072
|
<div
|
|
@@ -34820,8 +35076,8 @@
|
|
|
34820
35076
|
<div class="modal-card">
|
|
34821
35077
|
<div class="modal-header">
|
|
34822
35078
|
<div class="header-content">
|
|
34823
|
-
<div class="logo-icon">${
|
|
34824
|
-
<h2 class="modal-title"
|
|
35079
|
+
<div class="logo-icon">${this.modalIcon}</div>
|
|
35080
|
+
<h2 class="modal-title">${this.modalTitle}</h2>
|
|
34825
35081
|
</div>
|
|
34826
35082
|
<button
|
|
34827
35083
|
class="close-button"
|
|
@@ -34838,7 +35094,7 @@
|
|
|
34838
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">
|
|
34839
35095
|
<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
|
|
34840
35096
|
</svg>
|
|
34841
|
-
<span
|
|
35097
|
+
<span>${this.hintText}</span>
|
|
34842
35098
|
</div>
|
|
34843
35099
|
</div>
|
|
34844
35100
|
|
|
@@ -34865,6 +35121,9 @@
|
|
|
34865
35121
|
__decorate([
|
|
34866
35122
|
n({ type: String })
|
|
34867
35123
|
], SpeechOSDictationOutputModal.prototype, "text", void 0);
|
|
35124
|
+
__decorate([
|
|
35125
|
+
n({ type: String, reflect: true })
|
|
35126
|
+
], SpeechOSDictationOutputModal.prototype, "mode", void 0);
|
|
34868
35127
|
__decorate([
|
|
34869
35128
|
r()
|
|
34870
35129
|
], SpeechOSDictationOutputModal.prototype, "copied", void 0);
|
|
@@ -35024,15 +35283,29 @@
|
|
|
35024
35283
|
* duration so users can see the visual feedback before transitioning to recording.
|
|
35025
35284
|
*/
|
|
35026
35285
|
const MIN_CONNECTING_ANIMATION_MS = 200;
|
|
35286
|
+
/**
|
|
35287
|
+
* Time to wait for a transcription event before showing the "no audio" warning (in milliseconds).
|
|
35288
|
+
* If no transcription:interim event is received within this time during recording,
|
|
35289
|
+
* it indicates the server isn't receiving/processing audio.
|
|
35290
|
+
*/
|
|
35291
|
+
const NO_AUDIO_WARNING_TIMEOUT_MS = 5000;
|
|
35292
|
+
/**
|
|
35293
|
+
* Number of consecutive actions with empty results before showing warning on next action.
|
|
35294
|
+
*/
|
|
35295
|
+
const CONSECUTIVE_NO_AUDIO_THRESHOLD = 2;
|
|
35027
35296
|
let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
35028
35297
|
constructor() {
|
|
35029
35298
|
super(...arguments);
|
|
35030
35299
|
this.widgetState = state.getState();
|
|
35031
35300
|
this.settingsOpen = false;
|
|
35301
|
+
this.settingsOpenFromWarning = false;
|
|
35032
35302
|
this.dictationModalOpen = false;
|
|
35033
35303
|
this.dictationModalText = "";
|
|
35304
|
+
this.dictationModalMode = "dictation";
|
|
35034
35305
|
this.editHelpModalOpen = false;
|
|
35035
|
-
this.
|
|
35306
|
+
this.actionFeedback = null;
|
|
35307
|
+
this.showNoAudioWarning = false;
|
|
35308
|
+
this.isErrorRetryable = true;
|
|
35036
35309
|
this.dictationTargetElement = null;
|
|
35037
35310
|
this.editTargetElement = null;
|
|
35038
35311
|
this.dictationCursorStart = null;
|
|
@@ -35044,7 +35317,7 @@
|
|
|
35044
35317
|
this.modalElement = null;
|
|
35045
35318
|
this.dictationModalElement = null;
|
|
35046
35319
|
this.editHelpModalElement = null;
|
|
35047
|
-
this.
|
|
35320
|
+
this.actionFeedbackTimeout = null;
|
|
35048
35321
|
this.customPosition = null;
|
|
35049
35322
|
this.isDragging = false;
|
|
35050
35323
|
this.dragStartPos = null;
|
|
@@ -35054,6 +35327,11 @@
|
|
|
35054
35327
|
this.suppressNextClick = false;
|
|
35055
35328
|
this.boundViewportResizeHandler = null;
|
|
35056
35329
|
this.boundScrollHandler = null;
|
|
35330
|
+
// No-audio warning state tracking
|
|
35331
|
+
this.consecutiveNoAudioActions = 0;
|
|
35332
|
+
this.transcriptionReceived = false;
|
|
35333
|
+
this.noAudioWarningTimeout = null;
|
|
35334
|
+
this.transcriptionInterimUnsubscribe = null;
|
|
35057
35335
|
}
|
|
35058
35336
|
static { SpeechOSWidget_1 = this; }
|
|
35059
35337
|
static { this.styles = [
|
|
@@ -35136,6 +35414,7 @@
|
|
|
35136
35414
|
this.modalElement = document.createElement("speechos-settings-modal");
|
|
35137
35415
|
this.modalElement.addEventListener("modal-close", () => {
|
|
35138
35416
|
this.settingsOpen = false;
|
|
35417
|
+
this.settingsOpenFromWarning = false;
|
|
35139
35418
|
});
|
|
35140
35419
|
document.body.appendChild(this.modalElement);
|
|
35141
35420
|
// Mount dictation output modal
|
|
@@ -35151,7 +35430,17 @@
|
|
|
35151
35430
|
});
|
|
35152
35431
|
document.body.appendChild(this.editHelpModalElement);
|
|
35153
35432
|
this.stateUnsubscribe = state.subscribe((newState) => {
|
|
35154
|
-
if (!newState.isVisible
|
|
35433
|
+
if (!newState.isVisible) {
|
|
35434
|
+
if (getConfig().debug && this.settingsOpen) {
|
|
35435
|
+
console.log("[SpeechOS] Closing settings modal: widget hidden");
|
|
35436
|
+
}
|
|
35437
|
+
this.settingsOpen = false;
|
|
35438
|
+
this.settingsOpenFromWarning = false;
|
|
35439
|
+
}
|
|
35440
|
+
else if (!newState.isExpanded && !this.settingsOpenFromWarning) {
|
|
35441
|
+
if (getConfig().debug && this.settingsOpen) {
|
|
35442
|
+
console.log("[SpeechOS] Closing settings modal: widget collapsed");
|
|
35443
|
+
}
|
|
35155
35444
|
this.settingsOpen = false;
|
|
35156
35445
|
}
|
|
35157
35446
|
// Clear custom position when focused element changes (re-anchor to new element)
|
|
@@ -35165,6 +35454,8 @@
|
|
|
35165
35454
|
this.errorEventUnsubscribe = events.on("error", (payload) => {
|
|
35166
35455
|
if (this.widgetState.recordingState !== "idle" &&
|
|
35167
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";
|
|
35168
35459
|
state.setError(payload.message);
|
|
35169
35460
|
getBackend().disconnect().catch(() => { });
|
|
35170
35461
|
}
|
|
@@ -35197,9 +35488,9 @@
|
|
|
35197
35488
|
this.editHelpModalElement.remove();
|
|
35198
35489
|
this.editHelpModalElement = null;
|
|
35199
35490
|
}
|
|
35200
|
-
if (this.
|
|
35201
|
-
clearTimeout(this.
|
|
35202
|
-
this.
|
|
35491
|
+
if (this.actionFeedbackTimeout) {
|
|
35492
|
+
clearTimeout(this.actionFeedbackTimeout);
|
|
35493
|
+
this.actionFeedbackTimeout = null;
|
|
35203
35494
|
}
|
|
35204
35495
|
if (this.stateUnsubscribe) {
|
|
35205
35496
|
this.stateUnsubscribe();
|
|
@@ -35227,6 +35518,7 @@
|
|
|
35227
35518
|
window.removeEventListener("scroll", this.boundScrollHandler);
|
|
35228
35519
|
this.boundScrollHandler = null;
|
|
35229
35520
|
}
|
|
35521
|
+
this.cleanupNoAudioWarningTracking();
|
|
35230
35522
|
}
|
|
35231
35523
|
updated(changedProperties) {
|
|
35232
35524
|
if (changedProperties.has("settingsOpen") && this.modalElement) {
|
|
@@ -35238,6 +35530,9 @@
|
|
|
35238
35530
|
if (changedProperties.has("dictationModalText") && this.dictationModalElement) {
|
|
35239
35531
|
this.dictationModalElement.text = this.dictationModalText;
|
|
35240
35532
|
}
|
|
35533
|
+
if (changedProperties.has("dictationModalMode") && this.dictationModalElement) {
|
|
35534
|
+
this.dictationModalElement.mode = this.dictationModalMode;
|
|
35535
|
+
}
|
|
35241
35536
|
if (changedProperties.has("editHelpModalOpen") && this.editHelpModalElement) {
|
|
35242
35537
|
this.editHelpModalElement.open = this.editHelpModalOpen;
|
|
35243
35538
|
}
|
|
@@ -35403,7 +35698,7 @@
|
|
|
35403
35698
|
}
|
|
35404
35699
|
if (this.widgetState.recordingState === "idle") {
|
|
35405
35700
|
// Clear command feedback on any mic click
|
|
35406
|
-
this.
|
|
35701
|
+
this.clearActionFeedback();
|
|
35407
35702
|
// If we're expanding, prefetch the token to reduce latency when user selects an action
|
|
35408
35703
|
if (!this.widgetState.isExpanded) {
|
|
35409
35704
|
// Fire and forget - we don't need to wait for this (LiveKit only)
|
|
@@ -35420,6 +35715,8 @@
|
|
|
35420
35715
|
}
|
|
35421
35716
|
}
|
|
35422
35717
|
async handleStopRecording() {
|
|
35718
|
+
// Clean up no-audio warning tracking
|
|
35719
|
+
this.cleanupNoAudioWarningTracking();
|
|
35423
35720
|
if (this.widgetState.activeAction === "edit") {
|
|
35424
35721
|
await this.handleStopEdit();
|
|
35425
35722
|
}
|
|
@@ -35431,14 +35728,27 @@
|
|
|
35431
35728
|
const backend = getBackend();
|
|
35432
35729
|
try {
|
|
35433
35730
|
const transcription = await this.withMinDisplayTime(backend.stopVoiceSession(), 300);
|
|
35731
|
+
// Track result for consecutive failure detection
|
|
35732
|
+
this.trackActionResult(!!transcription);
|
|
35434
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
|
+
}
|
|
35435
35741
|
// Check if we have a target element to insert into
|
|
35436
35742
|
if (this.dictationTargetElement) {
|
|
35437
35743
|
this.insertTranscription(transcription);
|
|
35438
35744
|
}
|
|
35439
35745
|
else {
|
|
35440
35746
|
// No target element - show dictation output modal
|
|
35747
|
+
if (getConfig().debug) {
|
|
35748
|
+
console.log("[SpeechOS] No target element, showing dictation modal");
|
|
35749
|
+
}
|
|
35441
35750
|
this.dictationModalText = transcription;
|
|
35751
|
+
this.dictationModalMode = "dictation";
|
|
35442
35752
|
this.dictationModalOpen = true;
|
|
35443
35753
|
}
|
|
35444
35754
|
transcriptStore.saveTranscript(transcription, "dictate");
|
|
@@ -35450,6 +35760,8 @@
|
|
|
35450
35760
|
backend.startAutoRefresh?.();
|
|
35451
35761
|
}
|
|
35452
35762
|
catch (error) {
|
|
35763
|
+
// Track as failed result
|
|
35764
|
+
this.trackActionResult(false);
|
|
35453
35765
|
const errorMessage = error instanceof Error ? error.message : "Failed to transcribe audio";
|
|
35454
35766
|
if (errorMessage !== "Disconnected") {
|
|
35455
35767
|
state.setError(errorMessage);
|
|
@@ -35459,6 +35771,8 @@
|
|
|
35459
35771
|
}
|
|
35460
35772
|
}
|
|
35461
35773
|
async handleCancelOperation() {
|
|
35774
|
+
// Clean up no-audio warning tracking
|
|
35775
|
+
this.cleanupNoAudioWarningTracking();
|
|
35462
35776
|
await getBackend().disconnect();
|
|
35463
35777
|
if (this.widgetState.recordingState === "error") {
|
|
35464
35778
|
state.clearError();
|
|
@@ -35488,7 +35802,7 @@
|
|
|
35488
35802
|
}
|
|
35489
35803
|
}
|
|
35490
35804
|
handleCloseWidget() {
|
|
35491
|
-
this.
|
|
35805
|
+
this.clearActionFeedback();
|
|
35492
35806
|
state.hide();
|
|
35493
35807
|
}
|
|
35494
35808
|
handleSettingsClick() {
|
|
@@ -35578,45 +35892,70 @@
|
|
|
35578
35892
|
return;
|
|
35579
35893
|
}
|
|
35580
35894
|
const tagName = target.tagName.toLowerCase();
|
|
35895
|
+
const originalContent = this.getElementContent(target) || "";
|
|
35581
35896
|
if (tagName === "input" || tagName === "textarea") {
|
|
35582
35897
|
const inputEl = target;
|
|
35898
|
+
// Restore cursor position before inserting
|
|
35583
35899
|
const start = this.dictationCursorStart ?? inputEl.value.length;
|
|
35584
35900
|
const end = this.dictationCursorEnd ?? inputEl.value.length;
|
|
35585
|
-
|
|
35586
|
-
|
|
35587
|
-
inputEl
|
|
35588
|
-
if (this.supportsSelection(inputEl)) {
|
|
35589
|
-
const newCursorPos = start + text.length;
|
|
35590
|
-
inputEl.setSelectionRange(newCursorPos, newCursorPos);
|
|
35591
|
-
}
|
|
35592
|
-
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
|
|
35593
|
-
inputEl.focus();
|
|
35901
|
+
inputEl.setSelectionRange(start, end);
|
|
35902
|
+
// Use text-field-edit to insert text (handles undo, events, etc.)
|
|
35903
|
+
insertTextIntoField(inputEl, text);
|
|
35594
35904
|
state.setFocusedElement(inputEl);
|
|
35595
35905
|
}
|
|
35596
35906
|
else if (target.isContentEditable) {
|
|
35597
35907
|
target.focus();
|
|
35598
35908
|
state.setFocusedElement(target);
|
|
35599
|
-
|
|
35600
|
-
target
|
|
35601
|
-
const selection = window.getSelection();
|
|
35602
|
-
if (selection) {
|
|
35603
|
-
const range = document.createRange();
|
|
35604
|
-
range.selectNodeContents(textNode);
|
|
35605
|
-
range.collapse(false);
|
|
35606
|
-
selection.removeAllRanges();
|
|
35607
|
-
selection.addRange(range);
|
|
35608
|
-
}
|
|
35609
|
-
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
35909
|
+
// Use text-field-edit for contentEditable elements
|
|
35910
|
+
insertTextIntoField(target, text);
|
|
35610
35911
|
}
|
|
35611
35912
|
events.emit("transcription:inserted", { text, element: target });
|
|
35913
|
+
// Verify insertion was applied after DOM updates
|
|
35914
|
+
this.verifyInsertionApplied(target, text, originalContent);
|
|
35612
35915
|
this.dictationTargetElement = null;
|
|
35613
35916
|
this.dictationCursorStart = null;
|
|
35614
35917
|
this.dictationCursorEnd = null;
|
|
35615
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
|
+
}
|
|
35616
35955
|
handleActionSelect(event) {
|
|
35617
35956
|
const { action } = event.detail;
|
|
35618
35957
|
// Clear any existing command feedback when a new action is selected
|
|
35619
|
-
this.
|
|
35958
|
+
this.clearActionFeedback();
|
|
35620
35959
|
state.setActiveAction(action);
|
|
35621
35960
|
if (action === "dictate") {
|
|
35622
35961
|
this.startDictation();
|
|
@@ -35651,6 +35990,13 @@
|
|
|
35651
35990
|
this.dictationTargetElement = this.widgetState.focusedElement;
|
|
35652
35991
|
this.dictationCursorStart = null;
|
|
35653
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
|
+
}
|
|
35654
36000
|
if (this.dictationTargetElement) {
|
|
35655
36001
|
const tagName = this.dictationTargetElement.tagName.toLowerCase();
|
|
35656
36002
|
if (tagName === "input" || tagName === "textarea") {
|
|
@@ -35676,13 +36022,18 @@
|
|
|
35676
36022
|
// Ensure minimum animation duration before transitioning to recording
|
|
35677
36023
|
const elapsed = Date.now() - connectingStartTime;
|
|
35678
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
|
+
};
|
|
35679
36032
|
if (remainingDelay > 0) {
|
|
35680
|
-
setTimeout(
|
|
35681
|
-
state.setRecordingState("recording");
|
|
35682
|
-
}, remainingDelay);
|
|
36033
|
+
setTimeout(startRecording, remainingDelay);
|
|
35683
36034
|
}
|
|
35684
36035
|
else {
|
|
35685
|
-
|
|
36036
|
+
startRecording();
|
|
35686
36037
|
}
|
|
35687
36038
|
},
|
|
35688
36039
|
});
|
|
@@ -35690,7 +36041,10 @@
|
|
|
35690
36041
|
catch (error) {
|
|
35691
36042
|
const errorMessage = error instanceof Error ? error.message : "Connection failed";
|
|
35692
36043
|
if (errorMessage !== "Disconnected") {
|
|
35693
|
-
state
|
|
36044
|
+
// Only set error if not already in error state (error event may have already set it)
|
|
36045
|
+
if (this.widgetState.recordingState !== "error") {
|
|
36046
|
+
state.setError(`Failed to connect: ${errorMessage}`);
|
|
36047
|
+
}
|
|
35694
36048
|
await backend.disconnect();
|
|
35695
36049
|
}
|
|
35696
36050
|
}
|
|
@@ -35700,6 +36054,13 @@
|
|
|
35700
36054
|
this.editSelectionStart = null;
|
|
35701
36055
|
this.editSelectionEnd = null;
|
|
35702
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
|
+
}
|
|
35703
36064
|
if (this.editTargetElement) {
|
|
35704
36065
|
const tagName = this.editTargetElement.tagName.toLowerCase();
|
|
35705
36066
|
if (tagName === "input" || tagName === "textarea") {
|
|
@@ -35710,7 +36071,8 @@
|
|
|
35710
36071
|
const start = this.editSelectionStart ?? 0;
|
|
35711
36072
|
const end = this.editSelectionEnd ?? 0;
|
|
35712
36073
|
if (start !== end) {
|
|
35713
|
-
|
|
36074
|
+
// Use getFieldSelection from text-field-edit
|
|
36075
|
+
this.editSelectedText = getFieldSelection(inputEl);
|
|
35714
36076
|
}
|
|
35715
36077
|
}
|
|
35716
36078
|
else {
|
|
@@ -35719,13 +36081,11 @@
|
|
|
35719
36081
|
}
|
|
35720
36082
|
}
|
|
35721
36083
|
else if (this.editTargetElement.isContentEditable) {
|
|
35722
|
-
|
|
35723
|
-
|
|
35724
|
-
|
|
35725
|
-
|
|
35726
|
-
|
|
35727
|
-
this.editSelectedText = selectedText;
|
|
35728
|
-
}
|
|
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;
|
|
35729
36089
|
}
|
|
35730
36090
|
}
|
|
35731
36091
|
// Capture the content to edit at start time (sent with auth message)
|
|
@@ -35742,13 +36102,18 @@
|
|
|
35742
36102
|
// Ensure minimum animation duration before transitioning to recording
|
|
35743
36103
|
const elapsed = Date.now() - connectingStartTime;
|
|
35744
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
|
+
};
|
|
35745
36112
|
if (remainingDelay > 0) {
|
|
35746
|
-
setTimeout(
|
|
35747
|
-
state.setRecordingState("recording");
|
|
35748
|
-
}, remainingDelay);
|
|
36113
|
+
setTimeout(startRecording, remainingDelay);
|
|
35749
36114
|
}
|
|
35750
36115
|
else {
|
|
35751
|
-
|
|
36116
|
+
startRecording();
|
|
35752
36117
|
}
|
|
35753
36118
|
},
|
|
35754
36119
|
});
|
|
@@ -35756,7 +36121,10 @@
|
|
|
35756
36121
|
catch (error) {
|
|
35757
36122
|
const errorMessage = error instanceof Error ? error.message : "Connection failed";
|
|
35758
36123
|
if (errorMessage !== "Disconnected") {
|
|
35759
|
-
state
|
|
36124
|
+
// Only set error if not already in error state (error event may have already set it)
|
|
36125
|
+
if (this.widgetState.recordingState !== "error") {
|
|
36126
|
+
state.setError(`Failed to connect: ${errorMessage}`);
|
|
36127
|
+
}
|
|
35760
36128
|
await backend.disconnect();
|
|
35761
36129
|
}
|
|
35762
36130
|
}
|
|
@@ -35767,12 +36135,30 @@
|
|
|
35767
36135
|
const backend = getBackend();
|
|
35768
36136
|
try {
|
|
35769
36137
|
const editedText = await this.withMinDisplayTime(backend.requestEditText(originalContent), 300);
|
|
36138
|
+
// Check if server returned no change (couldn't understand edit)
|
|
36139
|
+
const noChange = editedText.trim() === originalContent.trim();
|
|
36140
|
+
if (noChange) {
|
|
36141
|
+
this.trackActionResult(false);
|
|
36142
|
+
this.showActionFeedback("edit-empty");
|
|
36143
|
+
state.completeRecording();
|
|
36144
|
+
this.editTargetElement = null;
|
|
36145
|
+
this.editSelectionStart = null;
|
|
36146
|
+
this.editSelectionEnd = null;
|
|
36147
|
+
this.editSelectedText = "";
|
|
36148
|
+
backend.disconnect().catch(() => { });
|
|
36149
|
+
backend.startAutoRefresh?.();
|
|
36150
|
+
return;
|
|
36151
|
+
}
|
|
36152
|
+
// Track result - got a meaningful change
|
|
36153
|
+
this.trackActionResult(true);
|
|
35770
36154
|
this.applyEdit(editedText);
|
|
35771
36155
|
backend.disconnect().catch(() => { });
|
|
35772
36156
|
// Start auto-refresh to keep token fresh for subsequent commands (LiveKit only)
|
|
35773
36157
|
backend.startAutoRefresh?.();
|
|
35774
36158
|
}
|
|
35775
36159
|
catch (error) {
|
|
36160
|
+
// Track as failed result
|
|
36161
|
+
this.trackActionResult(false);
|
|
35776
36162
|
const errorMessage = error instanceof Error ? error.message : "Failed to apply edit";
|
|
35777
36163
|
if (errorMessage !== "Disconnected") {
|
|
35778
36164
|
state.setError(errorMessage);
|
|
@@ -35795,13 +36181,18 @@
|
|
|
35795
36181
|
// Ensure minimum animation duration before transitioning to recording
|
|
35796
36182
|
const elapsed = Date.now() - connectingStartTime;
|
|
35797
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
|
+
};
|
|
35798
36191
|
if (remainingDelay > 0) {
|
|
35799
|
-
setTimeout(
|
|
35800
|
-
state.setRecordingState("recording");
|
|
35801
|
-
}, remainingDelay);
|
|
36192
|
+
setTimeout(startRecording, remainingDelay);
|
|
35802
36193
|
}
|
|
35803
36194
|
else {
|
|
35804
|
-
|
|
36195
|
+
startRecording();
|
|
35805
36196
|
}
|
|
35806
36197
|
},
|
|
35807
36198
|
});
|
|
@@ -35809,7 +36200,10 @@
|
|
|
35809
36200
|
catch (error) {
|
|
35810
36201
|
const errorMessage = error instanceof Error ? error.message : "Connection failed";
|
|
35811
36202
|
if (errorMessage !== "Disconnected") {
|
|
35812
|
-
state
|
|
36203
|
+
// Only set error if not already in error state (error event may have already set it)
|
|
36204
|
+
if (this.widgetState.recordingState !== "error") {
|
|
36205
|
+
state.setError(`Failed to connect: ${errorMessage}`);
|
|
36206
|
+
}
|
|
35813
36207
|
await backend.disconnect();
|
|
35814
36208
|
}
|
|
35815
36209
|
}
|
|
@@ -35821,6 +36215,8 @@
|
|
|
35821
36215
|
const backend = getBackend();
|
|
35822
36216
|
try {
|
|
35823
36217
|
const result = await this.withMinDisplayTime(backend.requestCommand(commands), 300);
|
|
36218
|
+
// Track result - null result means no command matched (possibly no audio)
|
|
36219
|
+
this.trackActionResult(result !== null);
|
|
35824
36220
|
// Get input text from the backend if available
|
|
35825
36221
|
const inputText = backend.getLastInputText?.();
|
|
35826
36222
|
// Save to transcript store
|
|
@@ -35838,12 +36234,14 @@
|
|
|
35838
36234
|
// Keep widget visible but collapsed (just mic button, no action bubbles)
|
|
35839
36235
|
state.setState({ isExpanded: false });
|
|
35840
36236
|
// Show command feedback
|
|
35841
|
-
this.
|
|
36237
|
+
this.showActionFeedback(result ? "command-success" : "command-none");
|
|
35842
36238
|
backend.disconnect().catch(() => { });
|
|
35843
36239
|
// Start auto-refresh to keep token fresh for subsequent commands (LiveKit only)
|
|
35844
36240
|
backend.startAutoRefresh?.();
|
|
35845
36241
|
}
|
|
35846
36242
|
catch (error) {
|
|
36243
|
+
// Track as failed result
|
|
36244
|
+
this.trackActionResult(false);
|
|
35847
36245
|
const errorMessage = error instanceof Error ? error.message : "Failed to process command";
|
|
35848
36246
|
if (errorMessage !== "Disconnected") {
|
|
35849
36247
|
state.setError(errorMessage);
|
|
@@ -35851,24 +36249,110 @@
|
|
|
35851
36249
|
}
|
|
35852
36250
|
}
|
|
35853
36251
|
}
|
|
35854
|
-
|
|
35855
|
-
this.
|
|
36252
|
+
showActionFeedback(feedback) {
|
|
36253
|
+
this.actionFeedback = feedback;
|
|
35856
36254
|
// Clear any existing timeout
|
|
35857
|
-
if (this.
|
|
35858
|
-
clearTimeout(this.
|
|
36255
|
+
if (this.actionFeedbackTimeout) {
|
|
36256
|
+
clearTimeout(this.actionFeedbackTimeout);
|
|
35859
36257
|
}
|
|
35860
36258
|
// Auto-dismiss after 4 seconds
|
|
35861
|
-
this.
|
|
35862
|
-
this.
|
|
35863
|
-
this.
|
|
36259
|
+
this.actionFeedbackTimeout = window.setTimeout(() => {
|
|
36260
|
+
this.actionFeedback = null;
|
|
36261
|
+
this.actionFeedbackTimeout = null;
|
|
35864
36262
|
}, 4000);
|
|
35865
36263
|
}
|
|
35866
|
-
|
|
35867
|
-
if (this.
|
|
35868
|
-
clearTimeout(this.
|
|
35869
|
-
this.
|
|
36264
|
+
clearActionFeedback() {
|
|
36265
|
+
if (this.actionFeedbackTimeout) {
|
|
36266
|
+
clearTimeout(this.actionFeedbackTimeout);
|
|
36267
|
+
this.actionFeedbackTimeout = null;
|
|
35870
36268
|
}
|
|
35871
|
-
this.
|
|
36269
|
+
this.actionFeedback = null;
|
|
36270
|
+
}
|
|
36271
|
+
/**
|
|
36272
|
+
* Start tracking for no-audio warning when recording begins.
|
|
36273
|
+
*/
|
|
36274
|
+
startNoAudioWarningTracking() {
|
|
36275
|
+
this.transcriptionReceived = false;
|
|
36276
|
+
this.showNoAudioWarning = false;
|
|
36277
|
+
// If we had consecutive failures, show warning immediately
|
|
36278
|
+
if (this.consecutiveNoAudioActions >= CONSECUTIVE_NO_AUDIO_THRESHOLD) {
|
|
36279
|
+
this.showNoAudioWarning = true;
|
|
36280
|
+
}
|
|
36281
|
+
// Start timeout - if no transcription within 5s, show warning
|
|
36282
|
+
this.noAudioWarningTimeout = window.setTimeout(() => {
|
|
36283
|
+
if (!this.transcriptionReceived &&
|
|
36284
|
+
this.widgetState.recordingState === "recording") {
|
|
36285
|
+
this.showNoAudioWarning = true;
|
|
36286
|
+
}
|
|
36287
|
+
}, NO_AUDIO_WARNING_TIMEOUT_MS);
|
|
36288
|
+
// Subscribe to transcription:interim events
|
|
36289
|
+
this.transcriptionInterimUnsubscribe = events.on("transcription:interim", () => {
|
|
36290
|
+
this.transcriptionReceived = true;
|
|
36291
|
+
if (this.showNoAudioWarning) {
|
|
36292
|
+
this.showNoAudioWarning = false;
|
|
36293
|
+
}
|
|
36294
|
+
});
|
|
36295
|
+
}
|
|
36296
|
+
/**
|
|
36297
|
+
* Clean up no-audio warning tracking when recording stops.
|
|
36298
|
+
*/
|
|
36299
|
+
cleanupNoAudioWarningTracking() {
|
|
36300
|
+
if (this.noAudioWarningTimeout !== null) {
|
|
36301
|
+
clearTimeout(this.noAudioWarningTimeout);
|
|
36302
|
+
this.noAudioWarningTimeout = null;
|
|
36303
|
+
}
|
|
36304
|
+
if (this.transcriptionInterimUnsubscribe) {
|
|
36305
|
+
this.transcriptionInterimUnsubscribe();
|
|
36306
|
+
this.transcriptionInterimUnsubscribe = null;
|
|
36307
|
+
}
|
|
36308
|
+
this.showNoAudioWarning = false;
|
|
36309
|
+
}
|
|
36310
|
+
/**
|
|
36311
|
+
* Track the result of an action for consecutive failure detection.
|
|
36312
|
+
*/
|
|
36313
|
+
trackActionResult(hasContent) {
|
|
36314
|
+
if (hasContent) {
|
|
36315
|
+
this.consecutiveNoAudioActions = 0;
|
|
36316
|
+
}
|
|
36317
|
+
else {
|
|
36318
|
+
this.consecutiveNoAudioActions++;
|
|
36319
|
+
}
|
|
36320
|
+
}
|
|
36321
|
+
/**
|
|
36322
|
+
* Handle opening settings from the no-audio warning.
|
|
36323
|
+
* Stops the current dictation session immediately, then opens settings.
|
|
36324
|
+
*/
|
|
36325
|
+
async handleOpenSettingsFromWarning() {
|
|
36326
|
+
if (getConfig().debug) {
|
|
36327
|
+
console.log("[SpeechOS] No-audio settings link clicked");
|
|
36328
|
+
}
|
|
36329
|
+
// Clean up no-audio warning tracking first
|
|
36330
|
+
this.cleanupNoAudioWarningTracking();
|
|
36331
|
+
// Keep settings open even if widget collapses
|
|
36332
|
+
this.settingsOpenFromWarning = true;
|
|
36333
|
+
// Stop audio capture and disconnect immediately (don't wait for transcription)
|
|
36334
|
+
// Kick this off before opening settings so audio stops fast, but don't block UI.
|
|
36335
|
+
const disconnectPromise = getBackend().disconnect().catch((error) => {
|
|
36336
|
+
if (getConfig().debug) {
|
|
36337
|
+
console.log("[SpeechOS] Disconnect failed while opening settings", error);
|
|
36338
|
+
}
|
|
36339
|
+
});
|
|
36340
|
+
// Update UI state to idle
|
|
36341
|
+
state.cancelRecording();
|
|
36342
|
+
// Clear target elements
|
|
36343
|
+
this.dictationTargetElement = null;
|
|
36344
|
+
this.editTargetElement = null;
|
|
36345
|
+
this.dictationCursorStart = null;
|
|
36346
|
+
this.dictationCursorEnd = null;
|
|
36347
|
+
this.editSelectionStart = null;
|
|
36348
|
+
this.editSelectionEnd = null;
|
|
36349
|
+
this.editSelectedText = "";
|
|
36350
|
+
// Open settings modal
|
|
36351
|
+
this.settingsOpen = true;
|
|
36352
|
+
if (getConfig().debug) {
|
|
36353
|
+
console.log("[SpeechOS] Settings modal opened from no-audio warning");
|
|
36354
|
+
}
|
|
36355
|
+
await disconnectPromise;
|
|
35872
36356
|
}
|
|
35873
36357
|
supportsSelection(element) {
|
|
35874
36358
|
if (element.tagName.toLowerCase() === "textarea") {
|
|
@@ -35884,21 +36368,14 @@
|
|
|
35884
36368
|
const tagName = element.tagName.toLowerCase();
|
|
35885
36369
|
if (tagName === "input" || tagName === "textarea") {
|
|
35886
36370
|
const inputEl = element;
|
|
35887
|
-
const
|
|
35888
|
-
|
|
35889
|
-
|
|
35890
|
-
const hasSelection = start !== end;
|
|
35891
|
-
if (hasSelection) {
|
|
35892
|
-
return fullContent.substring(start, end);
|
|
35893
|
-
}
|
|
35894
|
-
return fullContent;
|
|
36371
|
+
const selectedText = getFieldSelection(inputEl);
|
|
36372
|
+
// If there's selected text, return it; otherwise return full content
|
|
36373
|
+
return selectedText || inputEl.value;
|
|
35895
36374
|
}
|
|
35896
36375
|
else if (element.isContentEditable) {
|
|
35897
|
-
const
|
|
35898
|
-
|
|
35899
|
-
|
|
35900
|
-
}
|
|
35901
|
-
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 || "";
|
|
35902
36379
|
}
|
|
35903
36380
|
return "";
|
|
35904
36381
|
}
|
|
@@ -35913,40 +36390,44 @@
|
|
|
35913
36390
|
if (tagName === "input" || tagName === "textarea") {
|
|
35914
36391
|
const inputEl = target;
|
|
35915
36392
|
originalContent = inputEl.value;
|
|
35916
|
-
|
|
35917
|
-
|
|
35918
|
-
|
|
35919
|
-
|
|
35920
|
-
|
|
35921
|
-
|
|
35922
|
-
|
|
35923
|
-
|
|
35924
|
-
else {
|
|
35925
|
-
inputEl.setSelectionRange(0, inputEl.value.length);
|
|
35926
|
-
}
|
|
35927
|
-
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);
|
|
35928
36401
|
}
|
|
35929
36402
|
else {
|
|
35930
|
-
|
|
35931
|
-
inputEl
|
|
36403
|
+
// No selection - replace entire content using setFieldText()
|
|
36404
|
+
setFieldText(inputEl, editedText);
|
|
35932
36405
|
}
|
|
35933
36406
|
state.setFocusedElement(inputEl);
|
|
35934
36407
|
}
|
|
35935
36408
|
else if (target.isContentEditable) {
|
|
35936
36409
|
originalContent = target.textContent || "";
|
|
35937
|
-
target.focus();
|
|
35938
|
-
state.setFocusedElement(target);
|
|
35939
36410
|
const hasSelection = this.editSelectionStart !== null &&
|
|
35940
36411
|
this.editSelectionEnd !== null &&
|
|
35941
36412
|
this.editSelectionStart !== this.editSelectionEnd;
|
|
35942
|
-
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();
|
|
35943
36421
|
const selection = window.getSelection();
|
|
35944
|
-
|
|
35945
|
-
|
|
35946
|
-
|
|
35947
|
-
|
|
36422
|
+
if (selection) {
|
|
36423
|
+
const range = document.createRange();
|
|
36424
|
+
range.selectNodeContents(target);
|
|
36425
|
+
selection.removeAllRanges();
|
|
36426
|
+
selection.addRange(range);
|
|
36427
|
+
}
|
|
36428
|
+
insertTextIntoField(target, editedText);
|
|
35948
36429
|
}
|
|
35949
|
-
|
|
36430
|
+
state.setFocusedElement(target);
|
|
35950
36431
|
}
|
|
35951
36432
|
transcriptStore.saveTranscript(editedText, "edit", originalContent);
|
|
35952
36433
|
events.emit("edit:applied", {
|
|
@@ -35955,11 +36436,54 @@
|
|
|
35955
36436
|
element: target,
|
|
35956
36437
|
});
|
|
35957
36438
|
state.completeRecording();
|
|
36439
|
+
// Verify edit was applied after DOM updates
|
|
36440
|
+
this.verifyEditApplied(target, editedText, originalContent);
|
|
35958
36441
|
this.editTargetElement = null;
|
|
35959
36442
|
this.editSelectionStart = null;
|
|
35960
36443
|
this.editSelectionEnd = null;
|
|
35961
36444
|
this.editSelectedText = "";
|
|
35962
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
|
+
}
|
|
35963
36487
|
render() {
|
|
35964
36488
|
if (!this.widgetState.isVisible) {
|
|
35965
36489
|
this.setAttribute("hidden", "");
|
|
@@ -35987,12 +36511,15 @@
|
|
|
35987
36511
|
activeAction="${this.widgetState.activeAction || ""}"
|
|
35988
36512
|
editPreviewText="${this.editSelectedText}"
|
|
35989
36513
|
errorMessage="${this.widgetState.errorMessage || ""}"
|
|
35990
|
-
|
|
36514
|
+
?showRetryButton="${this.isErrorRetryable}"
|
|
36515
|
+
.actionFeedback="${this.actionFeedback}"
|
|
36516
|
+
?showNoAudioWarning="${this.showNoAudioWarning}"
|
|
35991
36517
|
@mic-click="${this.handleMicClick}"
|
|
35992
36518
|
@stop-recording="${this.handleStopRecording}"
|
|
35993
36519
|
@cancel-operation="${this.handleCancelOperation}"
|
|
35994
36520
|
@retry-connection="${this.handleRetryConnection}"
|
|
35995
36521
|
@close-widget="${this.handleCloseWidget}"
|
|
36522
|
+
@open-settings="${this.handleOpenSettingsFromWarning}"
|
|
35996
36523
|
></speechos-mic-button>
|
|
35997
36524
|
</div>
|
|
35998
36525
|
</div>
|
|
@@ -36011,12 +36538,21 @@
|
|
|
36011
36538
|
__decorate([
|
|
36012
36539
|
r()
|
|
36013
36540
|
], SpeechOSWidget.prototype, "dictationModalText", void 0);
|
|
36541
|
+
__decorate([
|
|
36542
|
+
r()
|
|
36543
|
+
], SpeechOSWidget.prototype, "dictationModalMode", void 0);
|
|
36014
36544
|
__decorate([
|
|
36015
36545
|
r()
|
|
36016
36546
|
], SpeechOSWidget.prototype, "editHelpModalOpen", void 0);
|
|
36017
36547
|
__decorate([
|
|
36018
36548
|
r()
|
|
36019
|
-
], SpeechOSWidget.prototype, "
|
|
36549
|
+
], SpeechOSWidget.prototype, "actionFeedback", void 0);
|
|
36550
|
+
__decorate([
|
|
36551
|
+
r()
|
|
36552
|
+
], SpeechOSWidget.prototype, "showNoAudioWarning", void 0);
|
|
36553
|
+
__decorate([
|
|
36554
|
+
r()
|
|
36555
|
+
], SpeechOSWidget.prototype, "isErrorRetryable", void 0);
|
|
36020
36556
|
SpeechOSWidget = SpeechOSWidget_1 = __decorate([
|
|
36021
36557
|
t$1("speechos-widget")
|
|
36022
36558
|
], SpeechOSWidget);
|