@speechos/client 0.2.5 → 0.2.6
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/index.cjs +320 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.iife.js +324 -41
- package/dist/index.iife.js.map +1 -1
- package/dist/index.iife.min.js +165 -67
- package/dist/index.iife.min.js.map +1 -1
- package/dist/index.js +321 -42
- package/dist/index.js.map +1 -1
- package/dist/ui/mic-button.d.ts +4 -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 +27 -4
- package/dist/ui/widget.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1760,7 +1760,8 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
1760
1760
|
this.activeAction = null;
|
|
1761
1761
|
this.editPreviewText = "";
|
|
1762
1762
|
this.errorMessage = null;
|
|
1763
|
-
this.
|
|
1763
|
+
this.actionFeedback = null;
|
|
1764
|
+
this.showNoAudioWarning = false;
|
|
1764
1765
|
}
|
|
1765
1766
|
static { this.styles = [
|
|
1766
1767
|
themeStyles,
|
|
@@ -2350,8 +2351,9 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
2350
2351
|
background-position: center;
|
|
2351
2352
|
}
|
|
2352
2353
|
|
|
2353
|
-
/* Command feedback badge - no match state (neutral gray) */
|
|
2354
|
-
.status-label.command-none
|
|
2354
|
+
/* Command/edit feedback badge - no match/empty state (neutral gray) */
|
|
2355
|
+
.status-label.command-none,
|
|
2356
|
+
.status-label.edit-empty {
|
|
2355
2357
|
background: #4b5563;
|
|
2356
2358
|
box-shadow: 0 4px 12px rgba(75, 85, 99, 0.3);
|
|
2357
2359
|
animation: command-feedback-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)
|
|
@@ -2508,6 +2510,60 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
2508
2510
|
border-color: rgba(255, 255, 255, 0.5);
|
|
2509
2511
|
}
|
|
2510
2512
|
|
|
2513
|
+
/* No audio warning banner */
|
|
2514
|
+
.no-audio-warning {
|
|
2515
|
+
position: absolute;
|
|
2516
|
+
bottom: 120px; /* Above button and waveform visualizer */
|
|
2517
|
+
left: 50%;
|
|
2518
|
+
transform: translateX(-50%) translateY(8px);
|
|
2519
|
+
display: flex;
|
|
2520
|
+
align-items: center;
|
|
2521
|
+
gap: 8px;
|
|
2522
|
+
padding: 10px 14px;
|
|
2523
|
+
border-radius: 12px;
|
|
2524
|
+
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
|
2525
|
+
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
|
2526
|
+
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
2527
|
+
pointer-events: none;
|
|
2528
|
+
opacity: 0;
|
|
2529
|
+
white-space: nowrap;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
.no-audio-warning.visible {
|
|
2533
|
+
opacity: 1;
|
|
2534
|
+
transform: translateX(-50%) translateY(0);
|
|
2535
|
+
pointer-events: auto;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
.no-audio-warning .warning-icon {
|
|
2539
|
+
flex-shrink: 0;
|
|
2540
|
+
color: white;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
.no-audio-warning .warning-text {
|
|
2544
|
+
font-size: 13px;
|
|
2545
|
+
font-weight: 500;
|
|
2546
|
+
color: white;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
.no-audio-warning .settings-link {
|
|
2550
|
+
background: rgba(255, 255, 255, 0.2);
|
|
2551
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
2552
|
+
border-radius: 6px;
|
|
2553
|
+
padding: 4px 10px;
|
|
2554
|
+
font-size: 12px;
|
|
2555
|
+
font-weight: 600;
|
|
2556
|
+
color: white;
|
|
2557
|
+
cursor: pointer;
|
|
2558
|
+
transition: all 0.15s;
|
|
2559
|
+
white-space: nowrap;
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
.no-audio-warning .settings-link:hover {
|
|
2563
|
+
background: rgba(255, 255, 255, 0.3);
|
|
2564
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2511
2567
|
/* Mobile styles - 30% larger */
|
|
2512
2568
|
@media (max-width: 768px) and (hover: none) {
|
|
2513
2569
|
.mic-button {
|
|
@@ -2637,6 +2693,21 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
2637
2693
|
padding: 8px 14px;
|
|
2638
2694
|
font-size: 14px;
|
|
2639
2695
|
}
|
|
2696
|
+
|
|
2697
|
+
.no-audio-warning {
|
|
2698
|
+
padding: 12px 16px;
|
|
2699
|
+
gap: 10px;
|
|
2700
|
+
bottom: 145px; /* Above button and waveform on mobile */
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
.no-audio-warning .warning-text {
|
|
2704
|
+
font-size: 15px;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
.no-audio-warning .settings-link {
|
|
2708
|
+
padding: 6px 12px;
|
|
2709
|
+
font-size: 14px;
|
|
2710
|
+
}
|
|
2640
2711
|
}
|
|
2641
2712
|
`,
|
|
2642
2713
|
]; }
|
|
@@ -2694,6 +2765,14 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
2694
2765
|
composed: true,
|
|
2695
2766
|
}));
|
|
2696
2767
|
}
|
|
2768
|
+
handleOpenSettings(e) {
|
|
2769
|
+
e.stopPropagation();
|
|
2770
|
+
e.preventDefault();
|
|
2771
|
+
this.dispatchEvent(new CustomEvent("open-settings", {
|
|
2772
|
+
bubbles: true,
|
|
2773
|
+
composed: true,
|
|
2774
|
+
}));
|
|
2775
|
+
}
|
|
2697
2776
|
getButtonClass() {
|
|
2698
2777
|
const classes = ["mic-button"];
|
|
2699
2778
|
if (this.expanded && this.recordingState === "idle") {
|
|
@@ -2778,13 +2857,16 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
2778
2857
|
}
|
|
2779
2858
|
return this.recordingState;
|
|
2780
2859
|
}
|
|
2781
|
-
|
|
2782
|
-
if (this.
|
|
2860
|
+
getActionFeedbackLabel() {
|
|
2861
|
+
if (this.actionFeedback === "command-success") {
|
|
2783
2862
|
return "Got it!";
|
|
2784
2863
|
}
|
|
2785
|
-
if (this.
|
|
2864
|
+
if (this.actionFeedback === "command-none") {
|
|
2786
2865
|
return "No command matched";
|
|
2787
2866
|
}
|
|
2867
|
+
if (this.actionFeedback === "edit-empty") {
|
|
2868
|
+
return "Couldn't understand edit";
|
|
2869
|
+
}
|
|
2788
2870
|
return "";
|
|
2789
2871
|
}
|
|
2790
2872
|
render() {
|
|
@@ -2794,9 +2876,9 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
2794
2876
|
const showSiriEdit = this.recordingState === "processing" && this.activeAction === "edit";
|
|
2795
2877
|
const statusLabel = this.getStatusLabel();
|
|
2796
2878
|
const showVisualizer = this.shouldShowVisualizer();
|
|
2797
|
-
// Show status label during recording (either visualizer or edit text) OR
|
|
2798
|
-
const
|
|
2799
|
-
const showStatus = this.recordingState === "recording" ||
|
|
2879
|
+
// Show status label during recording (either visualizer or edit text) OR action feedback
|
|
2880
|
+
const showActionFeedback = this.recordingState === "idle" && this.actionFeedback !== null;
|
|
2881
|
+
const showStatus = this.recordingState === "recording" || showActionFeedback;
|
|
2800
2882
|
const showCancel = this.recordingState === "connecting" ||
|
|
2801
2883
|
this.recordingState === "recording" ||
|
|
2802
2884
|
this.recordingState === "processing";
|
|
@@ -2835,6 +2917,35 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
2835
2917
|
`
|
|
2836
2918
|
: ""}
|
|
2837
2919
|
|
|
2920
|
+
<div
|
|
2921
|
+
class="no-audio-warning ${this.showNoAudioWarning &&
|
|
2922
|
+
this.recordingState === "recording"
|
|
2923
|
+
? "visible"
|
|
2924
|
+
: ""}"
|
|
2925
|
+
>
|
|
2926
|
+
<svg
|
|
2927
|
+
class="warning-icon"
|
|
2928
|
+
width="16"
|
|
2929
|
+
height="16"
|
|
2930
|
+
viewBox="0 0 24 24"
|
|
2931
|
+
fill="none"
|
|
2932
|
+
stroke="currentColor"
|
|
2933
|
+
stroke-width="2"
|
|
2934
|
+
stroke-linecap="round"
|
|
2935
|
+
stroke-linejoin="round"
|
|
2936
|
+
>
|
|
2937
|
+
<path
|
|
2938
|
+
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"
|
|
2939
|
+
/>
|
|
2940
|
+
<line x1="12" y1="9" x2="12" y2="13" />
|
|
2941
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
2942
|
+
</svg>
|
|
2943
|
+
<span class="warning-text">We're not hearing anything</span>
|
|
2944
|
+
<button class="settings-link" @click="${this.handleOpenSettings}">
|
|
2945
|
+
Check Settings
|
|
2946
|
+
</button>
|
|
2947
|
+
</div>
|
|
2948
|
+
|
|
2838
2949
|
<button
|
|
2839
2950
|
class="${this.getButtonClass()}"
|
|
2840
2951
|
@click="${this.handleClick}"
|
|
@@ -2847,14 +2958,14 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
|
|
|
2847
2958
|
</button>
|
|
2848
2959
|
|
|
2849
2960
|
<span
|
|
2850
|
-
class="status-label ${showStatus ? "visible" : ""} ${
|
|
2851
|
-
?
|
|
2961
|
+
class="status-label ${showStatus ? "visible" : ""} ${showActionFeedback
|
|
2962
|
+
? this.actionFeedback
|
|
2852
2963
|
: showVisualizer
|
|
2853
2964
|
? "visualizer"
|
|
2854
2965
|
: this.getStatusClass()}"
|
|
2855
2966
|
>
|
|
2856
|
-
${
|
|
2857
|
-
? this.
|
|
2967
|
+
${showActionFeedback
|
|
2968
|
+
? this.getActionFeedbackLabel()
|
|
2858
2969
|
: showVisualizer
|
|
2859
2970
|
? b `<speechos-audio-visualizer
|
|
2860
2971
|
?active="${showVisualizer}"
|
|
@@ -2902,7 +3013,10 @@ __decorate([
|
|
|
2902
3013
|
], SpeechOSMicButton.prototype, "errorMessage", void 0);
|
|
2903
3014
|
__decorate([
|
|
2904
3015
|
n({ type: String })
|
|
2905
|
-
], SpeechOSMicButton.prototype, "
|
|
3016
|
+
], SpeechOSMicButton.prototype, "actionFeedback", void 0);
|
|
3017
|
+
__decorate([
|
|
3018
|
+
n({ type: Boolean })
|
|
3019
|
+
], SpeechOSMicButton.prototype, "showNoAudioWarning", void 0);
|
|
2906
3020
|
SpeechOSMicButton = __decorate([
|
|
2907
3021
|
t$1("speechos-mic-button")
|
|
2908
3022
|
], SpeechOSMicButton);
|
|
@@ -6276,15 +6390,27 @@ var SpeechOSWidget_1;
|
|
|
6276
6390
|
* duration so users can see the visual feedback before transitioning to recording.
|
|
6277
6391
|
*/
|
|
6278
6392
|
const MIN_CONNECTING_ANIMATION_MS = 200;
|
|
6393
|
+
/**
|
|
6394
|
+
* Time to wait for a transcription event before showing the "no audio" warning (in milliseconds).
|
|
6395
|
+
* If no transcription:interim event is received within this time during recording,
|
|
6396
|
+
* it indicates the server isn't receiving/processing audio.
|
|
6397
|
+
*/
|
|
6398
|
+
const NO_AUDIO_WARNING_TIMEOUT_MS = 5000;
|
|
6399
|
+
/**
|
|
6400
|
+
* Number of consecutive actions with empty results before showing warning on next action.
|
|
6401
|
+
*/
|
|
6402
|
+
const CONSECUTIVE_NO_AUDIO_THRESHOLD = 2;
|
|
6279
6403
|
let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
6280
6404
|
constructor() {
|
|
6281
6405
|
super(...arguments);
|
|
6282
6406
|
this.widgetState = core.state.getState();
|
|
6283
6407
|
this.settingsOpen = false;
|
|
6408
|
+
this.settingsOpenFromWarning = false;
|
|
6284
6409
|
this.dictationModalOpen = false;
|
|
6285
6410
|
this.dictationModalText = "";
|
|
6286
6411
|
this.editHelpModalOpen = false;
|
|
6287
|
-
this.
|
|
6412
|
+
this.actionFeedback = null;
|
|
6413
|
+
this.showNoAudioWarning = false;
|
|
6288
6414
|
this.dictationTargetElement = null;
|
|
6289
6415
|
this.editTargetElement = null;
|
|
6290
6416
|
this.dictationCursorStart = null;
|
|
@@ -6296,7 +6422,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6296
6422
|
this.modalElement = null;
|
|
6297
6423
|
this.dictationModalElement = null;
|
|
6298
6424
|
this.editHelpModalElement = null;
|
|
6299
|
-
this.
|
|
6425
|
+
this.actionFeedbackTimeout = null;
|
|
6300
6426
|
this.customPosition = null;
|
|
6301
6427
|
this.isDragging = false;
|
|
6302
6428
|
this.dragStartPos = null;
|
|
@@ -6306,6 +6432,11 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6306
6432
|
this.suppressNextClick = false;
|
|
6307
6433
|
this.boundViewportResizeHandler = null;
|
|
6308
6434
|
this.boundScrollHandler = null;
|
|
6435
|
+
// No-audio warning state tracking
|
|
6436
|
+
this.consecutiveNoAudioActions = 0;
|
|
6437
|
+
this.transcriptionReceived = false;
|
|
6438
|
+
this.noAudioWarningTimeout = null;
|
|
6439
|
+
this.transcriptionInterimUnsubscribe = null;
|
|
6309
6440
|
}
|
|
6310
6441
|
static { SpeechOSWidget_1 = this; }
|
|
6311
6442
|
static { this.styles = [
|
|
@@ -6388,6 +6519,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6388
6519
|
this.modalElement = document.createElement("speechos-settings-modal");
|
|
6389
6520
|
this.modalElement.addEventListener("modal-close", () => {
|
|
6390
6521
|
this.settingsOpen = false;
|
|
6522
|
+
this.settingsOpenFromWarning = false;
|
|
6391
6523
|
});
|
|
6392
6524
|
document.body.appendChild(this.modalElement);
|
|
6393
6525
|
// Mount dictation output modal
|
|
@@ -6403,7 +6535,17 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6403
6535
|
});
|
|
6404
6536
|
document.body.appendChild(this.editHelpModalElement);
|
|
6405
6537
|
this.stateUnsubscribe = core.state.subscribe((newState) => {
|
|
6406
|
-
if (!newState.isVisible
|
|
6538
|
+
if (!newState.isVisible) {
|
|
6539
|
+
if (core.getConfig().debug && this.settingsOpen) {
|
|
6540
|
+
console.log("[SpeechOS] Closing settings modal: widget hidden");
|
|
6541
|
+
}
|
|
6542
|
+
this.settingsOpen = false;
|
|
6543
|
+
this.settingsOpenFromWarning = false;
|
|
6544
|
+
}
|
|
6545
|
+
else if (!newState.isExpanded && !this.settingsOpenFromWarning) {
|
|
6546
|
+
if (core.getConfig().debug && this.settingsOpen) {
|
|
6547
|
+
console.log("[SpeechOS] Closing settings modal: widget collapsed");
|
|
6548
|
+
}
|
|
6407
6549
|
this.settingsOpen = false;
|
|
6408
6550
|
}
|
|
6409
6551
|
// Clear custom position when focused element changes (re-anchor to new element)
|
|
@@ -6449,9 +6591,9 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6449
6591
|
this.editHelpModalElement.remove();
|
|
6450
6592
|
this.editHelpModalElement = null;
|
|
6451
6593
|
}
|
|
6452
|
-
if (this.
|
|
6453
|
-
clearTimeout(this.
|
|
6454
|
-
this.
|
|
6594
|
+
if (this.actionFeedbackTimeout) {
|
|
6595
|
+
clearTimeout(this.actionFeedbackTimeout);
|
|
6596
|
+
this.actionFeedbackTimeout = null;
|
|
6455
6597
|
}
|
|
6456
6598
|
if (this.stateUnsubscribe) {
|
|
6457
6599
|
this.stateUnsubscribe();
|
|
@@ -6479,6 +6621,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6479
6621
|
window.removeEventListener("scroll", this.boundScrollHandler);
|
|
6480
6622
|
this.boundScrollHandler = null;
|
|
6481
6623
|
}
|
|
6624
|
+
this.cleanupNoAudioWarningTracking();
|
|
6482
6625
|
}
|
|
6483
6626
|
updated(changedProperties) {
|
|
6484
6627
|
if (changedProperties.has("settingsOpen") && this.modalElement) {
|
|
@@ -6656,7 +6799,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6656
6799
|
}
|
|
6657
6800
|
if (this.widgetState.recordingState === "idle") {
|
|
6658
6801
|
// Clear command feedback on any mic click
|
|
6659
|
-
this.
|
|
6802
|
+
this.clearActionFeedback();
|
|
6660
6803
|
// If we're expanding, prefetch the token to reduce latency when user selects an action
|
|
6661
6804
|
if (!this.widgetState.isExpanded) {
|
|
6662
6805
|
// Fire and forget - we don't need to wait for this (LiveKit only)
|
|
@@ -6677,6 +6820,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6677
6820
|
}
|
|
6678
6821
|
}
|
|
6679
6822
|
async handleStopRecording() {
|
|
6823
|
+
// Clean up no-audio warning tracking
|
|
6824
|
+
this.cleanupNoAudioWarningTracking();
|
|
6680
6825
|
if (this.widgetState.activeAction === "edit") {
|
|
6681
6826
|
await this.handleStopEdit();
|
|
6682
6827
|
}
|
|
@@ -6688,6 +6833,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6688
6833
|
const backend = core.getBackend();
|
|
6689
6834
|
try {
|
|
6690
6835
|
const transcription = await this.withMinDisplayTime(backend.stopVoiceSession(), 300);
|
|
6836
|
+
// Track result for consecutive failure detection
|
|
6837
|
+
this.trackActionResult(!!transcription);
|
|
6691
6838
|
if (transcription) {
|
|
6692
6839
|
// Check if we have a target element to insert into
|
|
6693
6840
|
if (this.dictationTargetElement) {
|
|
@@ -6707,6 +6854,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6707
6854
|
backend.startAutoRefresh?.();
|
|
6708
6855
|
}
|
|
6709
6856
|
catch (error) {
|
|
6857
|
+
// Track as failed result
|
|
6858
|
+
this.trackActionResult(false);
|
|
6710
6859
|
const errorMessage = error instanceof Error ? error.message : "Failed to transcribe audio";
|
|
6711
6860
|
if (errorMessage !== "Disconnected") {
|
|
6712
6861
|
core.state.setError(errorMessage);
|
|
@@ -6716,6 +6865,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6716
6865
|
}
|
|
6717
6866
|
}
|
|
6718
6867
|
async handleCancelOperation() {
|
|
6868
|
+
// Clean up no-audio warning tracking
|
|
6869
|
+
this.cleanupNoAudioWarningTracking();
|
|
6719
6870
|
await core.getBackend().disconnect();
|
|
6720
6871
|
if (this.widgetState.recordingState === "error") {
|
|
6721
6872
|
core.state.clearError();
|
|
@@ -6745,7 +6896,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6745
6896
|
}
|
|
6746
6897
|
}
|
|
6747
6898
|
handleCloseWidget() {
|
|
6748
|
-
this.
|
|
6899
|
+
this.clearActionFeedback();
|
|
6749
6900
|
core.getBackend().stopAutoRefresh?.();
|
|
6750
6901
|
core.state.hide();
|
|
6751
6902
|
}
|
|
@@ -6874,7 +7025,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6874
7025
|
handleActionSelect(event) {
|
|
6875
7026
|
const { action } = event.detail;
|
|
6876
7027
|
// Clear any existing command feedback when a new action is selected
|
|
6877
|
-
this.
|
|
7028
|
+
this.clearActionFeedback();
|
|
6878
7029
|
core.state.setActiveAction(action);
|
|
6879
7030
|
if (action === "dictate") {
|
|
6880
7031
|
this.startDictation();
|
|
@@ -6937,10 +7088,12 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6937
7088
|
if (remainingDelay > 0) {
|
|
6938
7089
|
setTimeout(() => {
|
|
6939
7090
|
core.state.setRecordingState("recording");
|
|
7091
|
+
this.startNoAudioWarningTracking();
|
|
6940
7092
|
}, remainingDelay);
|
|
6941
7093
|
}
|
|
6942
7094
|
else {
|
|
6943
7095
|
core.state.setRecordingState("recording");
|
|
7096
|
+
this.startNoAudioWarningTracking();
|
|
6944
7097
|
}
|
|
6945
7098
|
},
|
|
6946
7099
|
});
|
|
@@ -6948,7 +7101,10 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
6948
7101
|
catch (error) {
|
|
6949
7102
|
const errorMessage = error instanceof Error ? error.message : "Connection failed";
|
|
6950
7103
|
if (errorMessage !== "Disconnected") {
|
|
6951
|
-
|
|
7104
|
+
// Only set error if not already in error state (error event may have already set it)
|
|
7105
|
+
if (this.widgetState.recordingState !== "error") {
|
|
7106
|
+
core.state.setError(`Failed to connect: ${errorMessage}`);
|
|
7107
|
+
}
|
|
6952
7108
|
await backend.disconnect();
|
|
6953
7109
|
}
|
|
6954
7110
|
}
|
|
@@ -7003,10 +7159,12 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7003
7159
|
if (remainingDelay > 0) {
|
|
7004
7160
|
setTimeout(() => {
|
|
7005
7161
|
core.state.setRecordingState("recording");
|
|
7162
|
+
this.startNoAudioWarningTracking();
|
|
7006
7163
|
}, remainingDelay);
|
|
7007
7164
|
}
|
|
7008
7165
|
else {
|
|
7009
7166
|
core.state.setRecordingState("recording");
|
|
7167
|
+
this.startNoAudioWarningTracking();
|
|
7010
7168
|
}
|
|
7011
7169
|
},
|
|
7012
7170
|
});
|
|
@@ -7014,7 +7172,10 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7014
7172
|
catch (error) {
|
|
7015
7173
|
const errorMessage = error instanceof Error ? error.message : "Connection failed";
|
|
7016
7174
|
if (errorMessage !== "Disconnected") {
|
|
7017
|
-
|
|
7175
|
+
// Only set error if not already in error state (error event may have already set it)
|
|
7176
|
+
if (this.widgetState.recordingState !== "error") {
|
|
7177
|
+
core.state.setError(`Failed to connect: ${errorMessage}`);
|
|
7178
|
+
}
|
|
7018
7179
|
await backend.disconnect();
|
|
7019
7180
|
}
|
|
7020
7181
|
}
|
|
@@ -7025,12 +7186,30 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7025
7186
|
const backend = core.getBackend();
|
|
7026
7187
|
try {
|
|
7027
7188
|
const editedText = await this.withMinDisplayTime(backend.requestEditText(originalContent), 300);
|
|
7189
|
+
// Check if server returned no change (couldn't understand edit)
|
|
7190
|
+
const noChange = editedText.trim() === originalContent.trim();
|
|
7191
|
+
if (noChange) {
|
|
7192
|
+
this.trackActionResult(false);
|
|
7193
|
+
this.showActionFeedback("edit-empty");
|
|
7194
|
+
core.state.completeRecording();
|
|
7195
|
+
this.editTargetElement = null;
|
|
7196
|
+
this.editSelectionStart = null;
|
|
7197
|
+
this.editSelectionEnd = null;
|
|
7198
|
+
this.editSelectedText = "";
|
|
7199
|
+
backend.disconnect().catch(() => { });
|
|
7200
|
+
backend.startAutoRefresh?.();
|
|
7201
|
+
return;
|
|
7202
|
+
}
|
|
7203
|
+
// Track result - got a meaningful change
|
|
7204
|
+
this.trackActionResult(true);
|
|
7028
7205
|
this.applyEdit(editedText);
|
|
7029
7206
|
backend.disconnect().catch(() => { });
|
|
7030
7207
|
// Start auto-refresh to keep token fresh for subsequent commands (LiveKit only)
|
|
7031
7208
|
backend.startAutoRefresh?.();
|
|
7032
7209
|
}
|
|
7033
7210
|
catch (error) {
|
|
7211
|
+
// Track as failed result
|
|
7212
|
+
this.trackActionResult(false);
|
|
7034
7213
|
const errorMessage = error instanceof Error ? error.message : "Failed to apply edit";
|
|
7035
7214
|
if (errorMessage !== "Disconnected") {
|
|
7036
7215
|
core.state.setError(errorMessage);
|
|
@@ -7056,10 +7235,12 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7056
7235
|
if (remainingDelay > 0) {
|
|
7057
7236
|
setTimeout(() => {
|
|
7058
7237
|
core.state.setRecordingState("recording");
|
|
7238
|
+
this.startNoAudioWarningTracking();
|
|
7059
7239
|
}, remainingDelay);
|
|
7060
7240
|
}
|
|
7061
7241
|
else {
|
|
7062
7242
|
core.state.setRecordingState("recording");
|
|
7243
|
+
this.startNoAudioWarningTracking();
|
|
7063
7244
|
}
|
|
7064
7245
|
},
|
|
7065
7246
|
});
|
|
@@ -7067,7 +7248,10 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7067
7248
|
catch (error) {
|
|
7068
7249
|
const errorMessage = error instanceof Error ? error.message : "Connection failed";
|
|
7069
7250
|
if (errorMessage !== "Disconnected") {
|
|
7070
|
-
|
|
7251
|
+
// Only set error if not already in error state (error event may have already set it)
|
|
7252
|
+
if (this.widgetState.recordingState !== "error") {
|
|
7253
|
+
core.state.setError(`Failed to connect: ${errorMessage}`);
|
|
7254
|
+
}
|
|
7071
7255
|
await backend.disconnect();
|
|
7072
7256
|
}
|
|
7073
7257
|
}
|
|
@@ -7079,6 +7263,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7079
7263
|
const backend = core.getBackend();
|
|
7080
7264
|
try {
|
|
7081
7265
|
const result = await this.withMinDisplayTime(backend.requestCommand(commands), 300);
|
|
7266
|
+
// Track result - null result means no command matched (possibly no audio)
|
|
7267
|
+
this.trackActionResult(result !== null);
|
|
7082
7268
|
// Get input text from the backend if available
|
|
7083
7269
|
const inputText = backend.getLastInputText?.();
|
|
7084
7270
|
// Save to transcript store
|
|
@@ -7096,12 +7282,14 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7096
7282
|
// Keep widget visible but collapsed (just mic button, no action bubbles)
|
|
7097
7283
|
core.state.setState({ isExpanded: false });
|
|
7098
7284
|
// Show command feedback
|
|
7099
|
-
this.
|
|
7285
|
+
this.showActionFeedback(result ? "command-success" : "command-none");
|
|
7100
7286
|
backend.disconnect().catch(() => { });
|
|
7101
7287
|
// Start auto-refresh to keep token fresh for subsequent commands (LiveKit only)
|
|
7102
7288
|
backend.startAutoRefresh?.();
|
|
7103
7289
|
}
|
|
7104
7290
|
catch (error) {
|
|
7291
|
+
// Track as failed result
|
|
7292
|
+
this.trackActionResult(false);
|
|
7105
7293
|
const errorMessage = error instanceof Error ? error.message : "Failed to process command";
|
|
7106
7294
|
if (errorMessage !== "Disconnected") {
|
|
7107
7295
|
core.state.setError(errorMessage);
|
|
@@ -7109,24 +7297,110 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7109
7297
|
}
|
|
7110
7298
|
}
|
|
7111
7299
|
}
|
|
7112
|
-
|
|
7113
|
-
this.
|
|
7300
|
+
showActionFeedback(feedback) {
|
|
7301
|
+
this.actionFeedback = feedback;
|
|
7114
7302
|
// Clear any existing timeout
|
|
7115
|
-
if (this.
|
|
7116
|
-
clearTimeout(this.
|
|
7303
|
+
if (this.actionFeedbackTimeout) {
|
|
7304
|
+
clearTimeout(this.actionFeedbackTimeout);
|
|
7117
7305
|
}
|
|
7118
7306
|
// Auto-dismiss after 4 seconds
|
|
7119
|
-
this.
|
|
7120
|
-
this.
|
|
7121
|
-
this.
|
|
7307
|
+
this.actionFeedbackTimeout = window.setTimeout(() => {
|
|
7308
|
+
this.actionFeedback = null;
|
|
7309
|
+
this.actionFeedbackTimeout = null;
|
|
7122
7310
|
}, 4000);
|
|
7123
7311
|
}
|
|
7124
|
-
|
|
7125
|
-
if (this.
|
|
7126
|
-
clearTimeout(this.
|
|
7127
|
-
this.
|
|
7312
|
+
clearActionFeedback() {
|
|
7313
|
+
if (this.actionFeedbackTimeout) {
|
|
7314
|
+
clearTimeout(this.actionFeedbackTimeout);
|
|
7315
|
+
this.actionFeedbackTimeout = null;
|
|
7128
7316
|
}
|
|
7129
|
-
this.
|
|
7317
|
+
this.actionFeedback = null;
|
|
7318
|
+
}
|
|
7319
|
+
/**
|
|
7320
|
+
* Start tracking for no-audio warning when recording begins.
|
|
7321
|
+
*/
|
|
7322
|
+
startNoAudioWarningTracking() {
|
|
7323
|
+
this.transcriptionReceived = false;
|
|
7324
|
+
this.showNoAudioWarning = false;
|
|
7325
|
+
// If we had consecutive failures, show warning immediately
|
|
7326
|
+
if (this.consecutiveNoAudioActions >= CONSECUTIVE_NO_AUDIO_THRESHOLD) {
|
|
7327
|
+
this.showNoAudioWarning = true;
|
|
7328
|
+
}
|
|
7329
|
+
// Start timeout - if no transcription within 5s, show warning
|
|
7330
|
+
this.noAudioWarningTimeout = window.setTimeout(() => {
|
|
7331
|
+
if (!this.transcriptionReceived &&
|
|
7332
|
+
this.widgetState.recordingState === "recording") {
|
|
7333
|
+
this.showNoAudioWarning = true;
|
|
7334
|
+
}
|
|
7335
|
+
}, NO_AUDIO_WARNING_TIMEOUT_MS);
|
|
7336
|
+
// Subscribe to transcription:interim events
|
|
7337
|
+
this.transcriptionInterimUnsubscribe = core.events.on("transcription:interim", () => {
|
|
7338
|
+
this.transcriptionReceived = true;
|
|
7339
|
+
if (this.showNoAudioWarning) {
|
|
7340
|
+
this.showNoAudioWarning = false;
|
|
7341
|
+
}
|
|
7342
|
+
});
|
|
7343
|
+
}
|
|
7344
|
+
/**
|
|
7345
|
+
* Clean up no-audio warning tracking when recording stops.
|
|
7346
|
+
*/
|
|
7347
|
+
cleanupNoAudioWarningTracking() {
|
|
7348
|
+
if (this.noAudioWarningTimeout !== null) {
|
|
7349
|
+
clearTimeout(this.noAudioWarningTimeout);
|
|
7350
|
+
this.noAudioWarningTimeout = null;
|
|
7351
|
+
}
|
|
7352
|
+
if (this.transcriptionInterimUnsubscribe) {
|
|
7353
|
+
this.transcriptionInterimUnsubscribe();
|
|
7354
|
+
this.transcriptionInterimUnsubscribe = null;
|
|
7355
|
+
}
|
|
7356
|
+
this.showNoAudioWarning = false;
|
|
7357
|
+
}
|
|
7358
|
+
/**
|
|
7359
|
+
* Track the result of an action for consecutive failure detection.
|
|
7360
|
+
*/
|
|
7361
|
+
trackActionResult(hasContent) {
|
|
7362
|
+
if (hasContent) {
|
|
7363
|
+
this.consecutiveNoAudioActions = 0;
|
|
7364
|
+
}
|
|
7365
|
+
else {
|
|
7366
|
+
this.consecutiveNoAudioActions++;
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7369
|
+
/**
|
|
7370
|
+
* Handle opening settings from the no-audio warning.
|
|
7371
|
+
* Stops the current dictation session immediately, then opens settings.
|
|
7372
|
+
*/
|
|
7373
|
+
async handleOpenSettingsFromWarning() {
|
|
7374
|
+
if (core.getConfig().debug) {
|
|
7375
|
+
console.log("[SpeechOS] No-audio settings link clicked");
|
|
7376
|
+
}
|
|
7377
|
+
// Clean up no-audio warning tracking first
|
|
7378
|
+
this.cleanupNoAudioWarningTracking();
|
|
7379
|
+
// Keep settings open even if widget collapses
|
|
7380
|
+
this.settingsOpenFromWarning = true;
|
|
7381
|
+
// Stop audio capture and disconnect immediately (don't wait for transcription)
|
|
7382
|
+
// Kick this off before opening settings so audio stops fast, but don't block UI.
|
|
7383
|
+
const disconnectPromise = core.getBackend().disconnect().catch((error) => {
|
|
7384
|
+
if (core.getConfig().debug) {
|
|
7385
|
+
console.log("[SpeechOS] Disconnect failed while opening settings", error);
|
|
7386
|
+
}
|
|
7387
|
+
});
|
|
7388
|
+
// Update UI state to idle
|
|
7389
|
+
core.state.cancelRecording();
|
|
7390
|
+
// Clear target elements
|
|
7391
|
+
this.dictationTargetElement = null;
|
|
7392
|
+
this.editTargetElement = null;
|
|
7393
|
+
this.dictationCursorStart = null;
|
|
7394
|
+
this.dictationCursorEnd = null;
|
|
7395
|
+
this.editSelectionStart = null;
|
|
7396
|
+
this.editSelectionEnd = null;
|
|
7397
|
+
this.editSelectedText = "";
|
|
7398
|
+
// Open settings modal
|
|
7399
|
+
this.settingsOpen = true;
|
|
7400
|
+
if (core.getConfig().debug) {
|
|
7401
|
+
console.log("[SpeechOS] Settings modal opened from no-audio warning");
|
|
7402
|
+
}
|
|
7403
|
+
await disconnectPromise;
|
|
7130
7404
|
}
|
|
7131
7405
|
supportsSelection(element) {
|
|
7132
7406
|
if (element.tagName.toLowerCase() === "textarea") {
|
|
@@ -7245,12 +7519,14 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
|
|
|
7245
7519
|
activeAction="${this.widgetState.activeAction || ""}"
|
|
7246
7520
|
editPreviewText="${this.editSelectedText}"
|
|
7247
7521
|
errorMessage="${this.widgetState.errorMessage || ""}"
|
|
7248
|
-
.
|
|
7522
|
+
.actionFeedback="${this.actionFeedback}"
|
|
7523
|
+
?showNoAudioWarning="${this.showNoAudioWarning}"
|
|
7249
7524
|
@mic-click="${this.handleMicClick}"
|
|
7250
7525
|
@stop-recording="${this.handleStopRecording}"
|
|
7251
7526
|
@cancel-operation="${this.handleCancelOperation}"
|
|
7252
7527
|
@retry-connection="${this.handleRetryConnection}"
|
|
7253
7528
|
@close-widget="${this.handleCloseWidget}"
|
|
7529
|
+
@open-settings="${this.handleOpenSettingsFromWarning}"
|
|
7254
7530
|
></speechos-mic-button>
|
|
7255
7531
|
</div>
|
|
7256
7532
|
</div>
|
|
@@ -7274,7 +7550,10 @@ __decorate([
|
|
|
7274
7550
|
], SpeechOSWidget.prototype, "editHelpModalOpen", void 0);
|
|
7275
7551
|
__decorate([
|
|
7276
7552
|
r()
|
|
7277
|
-
], SpeechOSWidget.prototype, "
|
|
7553
|
+
], SpeechOSWidget.prototype, "actionFeedback", void 0);
|
|
7554
|
+
__decorate([
|
|
7555
|
+
r()
|
|
7556
|
+
], SpeechOSWidget.prototype, "showNoAudioWarning", void 0);
|
|
7278
7557
|
SpeechOSWidget = SpeechOSWidget_1 = __decorate([
|
|
7279
7558
|
t$1("speechos-widget")
|
|
7280
7559
|
], SpeechOSWidget);
|