@speechos/client 0.2.3 → 0.2.4

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.js CHANGED
@@ -1,6 +1,84 @@
1
1
  import { state, events, getBackend, getConfig, setConfig, updateUserId } from '@speechos/core';
2
2
  export { DEFAULT_HOST, events, getConfig, livekit, resetConfig, setConfig, state } from '@speechos/core';
3
3
 
4
+ /**
5
+ * Client configuration for SpeechOS
6
+ * Extends core config with UI/widget-specific options
7
+ */
8
+ /**
9
+ * Default client configuration values
10
+ */
11
+ const defaultClientConfig = {
12
+ commands: [],
13
+ zIndex: 999999,
14
+ alwaysVisible: false,
15
+ };
16
+ /**
17
+ * Current client configuration singleton
18
+ */
19
+ let currentClientConfig = { ...defaultClientConfig };
20
+ /**
21
+ * Validate and resolve client-specific config
22
+ * @param config - User-provided configuration
23
+ * @returns Resolved client configuration
24
+ */
25
+ function validateClientConfig(config) {
26
+ const resolved = {
27
+ commands: config.commands ?? defaultClientConfig.commands,
28
+ zIndex: config.zIndex ?? defaultClientConfig.zIndex,
29
+ alwaysVisible: config.alwaysVisible ?? defaultClientConfig.alwaysVisible,
30
+ };
31
+ // Validate zIndex
32
+ if (typeof resolved.zIndex !== "number" || resolved.zIndex < 0) {
33
+ console.warn(`Invalid zIndex "${resolved.zIndex}". Using default ${defaultClientConfig.zIndex}.`);
34
+ resolved.zIndex = defaultClientConfig.zIndex;
35
+ }
36
+ return resolved;
37
+ }
38
+ /**
39
+ * Set the client configuration
40
+ * @param config - Client configuration to set
41
+ */
42
+ function setClientConfig(config) {
43
+ currentClientConfig = validateClientConfig(config);
44
+ }
45
+ /**
46
+ * Get the current client configuration
47
+ */
48
+ function getClientConfig() {
49
+ return { ...currentClientConfig };
50
+ }
51
+ /**
52
+ * Reset client configuration to defaults
53
+ */
54
+ function resetClientConfig() {
55
+ currentClientConfig = { ...defaultClientConfig };
56
+ }
57
+ /**
58
+ * Check if commands are configured (for showing Command button in widget)
59
+ */
60
+ function hasCommands() {
61
+ return currentClientConfig.commands.length > 0;
62
+ }
63
+ /**
64
+ * Get configured commands
65
+ */
66
+ function getCommands() {
67
+ return [...currentClientConfig.commands];
68
+ }
69
+ /**
70
+ * Get widget z-index
71
+ */
72
+ function getZIndex() {
73
+ return currentClientConfig.zIndex;
74
+ }
75
+ /**
76
+ * Check if widget should always be visible
77
+ */
78
+ function isAlwaysVisible() {
79
+ return currentClientConfig.alwaysVisible;
80
+ }
81
+
4
82
  /**
5
83
  * Form field focus detection for SpeechOS Client SDK
6
84
  * Detects when users focus on form fields and manages widget visibility
@@ -88,12 +166,12 @@ class FormDetector {
88
166
  relatedTarget === widget);
89
167
  // If focus is going to an element with data-speechos-no-close, don't hide
90
168
  const goingToNoCloseElement = Boolean(relatedTarget?.closest("[data-speechos-no-close]"));
91
- console.log("[SpeechOS] blurHandler:", {
92
- relatedTarget,
93
- goingToFormField,
94
- goingToWidget,
95
- goingToNoCloseElement,
96
- });
169
+ // console.log("[SpeechOS] blurHandler:", {
170
+ // relatedTarget,
171
+ // goingToFormField,
172
+ // goingToWidget,
173
+ // goingToNoCloseElement,
174
+ // });
97
175
  if (goingToFormField || goingToWidget || goingToNoCloseElement) {
98
176
  console.log("[SpeechOS] blurHandler: early return, not hiding");
99
177
  return;
@@ -112,11 +190,15 @@ class FormDetector {
112
190
  // Check if focus is on an element with data-speechos-no-close
113
191
  const isNoCloseElementFocused = Boolean(activeElement?.closest("[data-speechos-no-close]"));
114
192
  // Only hide if no form field is focused AND widget isn't focused AND not a no-close element
193
+ // AND alwaysVisible is not enabled
115
194
  if (!isFormField(activeElement) &&
116
195
  !isWidgetFocused &&
117
196
  !isNoCloseElementFocused) {
118
197
  state.setFocusedElement(null);
119
- state.hide();
198
+ // Don't hide if alwaysVisible is enabled
199
+ if (!isAlwaysVisible()) {
200
+ state.hide();
201
+ }
120
202
  events.emit("form:blur", { element: null });
121
203
  }
122
204
  }, 150);
@@ -190,7 +272,10 @@ class FormDetector {
190
272
  // Reset state
191
273
  this.isWidgetBeingInteracted = false;
192
274
  state.setFocusedElement(null);
193
- state.hide();
275
+ // Don't hide if alwaysVisible is enabled
276
+ if (!isAlwaysVisible()) {
277
+ state.hide();
278
+ }
194
279
  this.isActive = false;
195
280
  }
196
281
  /**
@@ -203,76 +288,6 @@ class FormDetector {
203
288
  // Export singleton instance
204
289
  const formDetector = new FormDetector();
205
290
 
206
- /**
207
- * Client configuration for SpeechOS
208
- * Extends core config with UI/widget-specific options
209
- */
210
- /**
211
- * Default client configuration values
212
- */
213
- const defaultClientConfig = {
214
- commands: [],
215
- zIndex: 999999,
216
- };
217
- /**
218
- * Current client configuration singleton
219
- */
220
- let currentClientConfig = { ...defaultClientConfig };
221
- /**
222
- * Validate and resolve client-specific config
223
- * @param config - User-provided configuration
224
- * @returns Resolved client configuration
225
- */
226
- function validateClientConfig(config) {
227
- const resolved = {
228
- commands: config.commands ?? defaultClientConfig.commands,
229
- zIndex: config.zIndex ?? defaultClientConfig.zIndex,
230
- };
231
- // Validate zIndex
232
- if (typeof resolved.zIndex !== "number" || resolved.zIndex < 0) {
233
- console.warn(`Invalid zIndex "${resolved.zIndex}". Using default ${defaultClientConfig.zIndex}.`);
234
- resolved.zIndex = defaultClientConfig.zIndex;
235
- }
236
- return resolved;
237
- }
238
- /**
239
- * Set the client configuration
240
- * @param config - Client configuration to set
241
- */
242
- function setClientConfig(config) {
243
- currentClientConfig = validateClientConfig(config);
244
- }
245
- /**
246
- * Get the current client configuration
247
- */
248
- function getClientConfig() {
249
- return { ...currentClientConfig };
250
- }
251
- /**
252
- * Reset client configuration to defaults
253
- */
254
- function resetClientConfig() {
255
- currentClientConfig = { ...defaultClientConfig };
256
- }
257
- /**
258
- * Check if commands are configured (for showing Command button in widget)
259
- */
260
- function hasCommands() {
261
- return currentClientConfig.commands.length > 0;
262
- }
263
- /**
264
- * Get configured commands
265
- */
266
- function getCommands() {
267
- return [...currentClientConfig.commands];
268
- }
269
- /**
270
- * Get widget z-index
271
- */
272
- function getZIndex() {
273
- return currentClientConfig.zIndex;
274
- }
275
-
276
291
  /**
277
292
  * Text input handler for SpeechOS Client SDK
278
293
  * Abstracts cursor/selection detection and text insertion/replacement operations
@@ -1477,6 +1492,11 @@ const loaderIcon = (size = 20) => {
1477
1492
  `;
1478
1493
  return b `${o(svgString)}`;
1479
1494
  };
1495
+ /**
1496
+ * Check icon for "Keep" action
1497
+ * Lucide Check icon paths
1498
+ */
1499
+ const checkIcon = (size = 18) => createIcon('<path d="M20 6 9 17l-5-5"/>', size);
1480
1500
  /**
1481
1501
  * X icon for cancel action
1482
1502
  * Lucide X icon paths
@@ -1737,6 +1757,7 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
1737
1757
  this.activeAction = null;
1738
1758
  this.editPreviewText = "";
1739
1759
  this.errorMessage = null;
1760
+ this.commandFeedback = null;
1740
1761
  }
1741
1762
  static { this.styles = [
1742
1763
  themeStyles,
@@ -2304,6 +2325,47 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2304
2325
  }
2305
2326
  }
2306
2327
 
2328
+ /* Command feedback badge - success state (amber/orange) */
2329
+ .status-label.command-success {
2330
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
2331
+ box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
2332
+ padding-left: 24px;
2333
+ animation: command-feedback-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)
2334
+ forwards;
2335
+ }
2336
+
2337
+ .status-label.command-success::before {
2338
+ content: "";
2339
+ position: absolute;
2340
+ top: 50%;
2341
+ left: 8px;
2342
+ width: 12px;
2343
+ height: 12px;
2344
+ transform: translateY(-50%);
2345
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
2346
+ background-repeat: no-repeat;
2347
+ background-position: center;
2348
+ }
2349
+
2350
+ /* Command feedback badge - no match state (neutral gray) */
2351
+ .status-label.command-none {
2352
+ background: #4b5563;
2353
+ box-shadow: 0 4px 12px rgba(75, 85, 99, 0.3);
2354
+ animation: command-feedback-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)
2355
+ forwards;
2356
+ }
2357
+
2358
+ @keyframes command-feedback-in {
2359
+ 0% {
2360
+ opacity: 0;
2361
+ transform: translateX(-50%) scale(0.8) translateY(4px);
2362
+ }
2363
+ 100% {
2364
+ opacity: 1;
2365
+ transform: translateX(-50%) scale(1) translateY(0);
2366
+ }
2367
+ }
2368
+
2307
2369
  /* Cancel button - positioned to the right of the main mic button */
2308
2370
  .cancel-button {
2309
2371
  position: absolute;
@@ -2551,6 +2613,16 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2551
2613
  left: 10px;
2552
2614
  }
2553
2615
 
2616
+ .status-label.command-success {
2617
+ padding-left: 30px;
2618
+ }
2619
+
2620
+ .status-label.command-success::before {
2621
+ left: 10px;
2622
+ width: 14px;
2623
+ height: 14px;
2624
+ }
2625
+
2554
2626
  .error-message {
2555
2627
  font-size: 15px;
2556
2628
  padding: 14px 18px;
@@ -2703,6 +2775,15 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2703
2775
  }
2704
2776
  return this.recordingState;
2705
2777
  }
2778
+ getCommandFeedbackLabel() {
2779
+ if (this.commandFeedback === "success") {
2780
+ return "Got it!";
2781
+ }
2782
+ if (this.commandFeedback === "none") {
2783
+ return "No command matched";
2784
+ }
2785
+ return "";
2786
+ }
2706
2787
  render() {
2707
2788
  const showPulse = this.recordingState === "recording";
2708
2789
  const showSiriConnecting = this.recordingState === "connecting";
@@ -2710,13 +2791,14 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2710
2791
  const showSiriEdit = this.recordingState === "processing" && this.activeAction === "edit";
2711
2792
  const statusLabel = this.getStatusLabel();
2712
2793
  const showVisualizer = this.shouldShowVisualizer();
2713
- // Show status label during recording (either visualizer or edit text)
2714
- const showStatus = this.recordingState === "recording";
2794
+ // Show status label during recording (either visualizer or edit text) OR command feedback
2795
+ const showCommandFeedback = this.recordingState === "idle" && this.commandFeedback !== null;
2796
+ const showStatus = this.recordingState === "recording" || showCommandFeedback;
2715
2797
  const showCancel = this.recordingState === "connecting" ||
2716
2798
  this.recordingState === "recording" ||
2717
2799
  this.recordingState === "processing";
2718
2800
  const showError = this.recordingState === "error" && this.errorMessage;
2719
- // Show close button in idle state (both solo mic and expanded)
2801
+ // Show close button in idle state (both solo mic and expanded), including when showing command feedback
2720
2802
  const showClose = this.recordingState === "idle";
2721
2803
  return b `
2722
2804
  <div class="button-wrapper">
@@ -2762,15 +2844,19 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2762
2844
  </button>
2763
2845
 
2764
2846
  <span
2765
- class="status-label ${showStatus ? "visible" : ""} ${showVisualizer
2766
- ? "visualizer"
2767
- : this.getStatusClass()}"
2847
+ class="status-label ${showStatus ? "visible" : ""} ${showCommandFeedback
2848
+ ? `command-${this.commandFeedback}`
2849
+ : showVisualizer
2850
+ ? "visualizer"
2851
+ : this.getStatusClass()}"
2768
2852
  >
2769
- ${showVisualizer
2770
- ? b `<speechos-audio-visualizer
2771
- ?active="${showVisualizer}"
2772
- ></speechos-audio-visualizer>`
2773
- : statusLabel}
2853
+ ${showCommandFeedback
2854
+ ? this.getCommandFeedbackLabel()
2855
+ : showVisualizer
2856
+ ? b `<speechos-audio-visualizer
2857
+ ?active="${showVisualizer}"
2858
+ ></speechos-audio-visualizer>`
2859
+ : statusLabel}
2774
2860
  </span>
2775
2861
 
2776
2862
  <button
@@ -2811,6 +2897,9 @@ __decorate([
2811
2897
  __decorate([
2812
2898
  n({ type: String })
2813
2899
  ], SpeechOSMicButton.prototype, "errorMessage", void 0);
2900
+ __decorate([
2901
+ n({ type: String })
2902
+ ], SpeechOSMicButton.prototype, "commandFeedback", void 0);
2814
2903
  SpeechOSMicButton = __decorate([
2815
2904
  t$1("speechos-mic-button")
2816
2905
  ], SpeechOSMicButton);
@@ -5588,65 +5677,658 @@ SpeechOSSettingsModal = __decorate([
5588
5677
  ], SpeechOSSettingsModal);
5589
5678
 
5590
5679
  /**
5591
- * Main widget container component
5592
- * Composes mic button and action bubbles with state management
5593
- */
5594
- var SpeechOSWidget_1;
5595
- /**
5596
- * Minimum duration to show the connecting animation (in milliseconds).
5597
- * Since mic capture starts almost instantly, we enforce a minimum animation
5598
- * duration so users can see the visual feedback before transitioning to recording.
5680
+ * Shared styles for lightweight popup modals
5681
+ * Used by dictation-output-modal and edit-help-modal
5599
5682
  */
5600
- const MIN_CONNECTING_ANIMATION_MS = 200;
5601
- let SpeechOSWidget = class SpeechOSWidget extends i$1 {
5602
- constructor() {
5603
- super(...arguments);
5604
- this.widgetState = state.getState();
5605
- this.settingsOpen = false;
5606
- this.dictationTargetElement = null;
5607
- this.editTargetElement = null;
5608
- this.dictationCursorStart = null;
5609
- this.dictationCursorEnd = null;
5610
- this.editSelectionStart = null;
5611
- this.editSelectionEnd = null;
5612
- this.editSelectedText = "";
5613
- this.boundClickOutsideHandler = null;
5614
- this.modalElement = null;
5615
- this.customPosition = null;
5616
- this.isDragging = false;
5617
- this.dragStartPos = null;
5618
- this.dragOffset = { x: 0, y: 0 };
5619
- this.boundDragMove = null;
5620
- this.boundDragEnd = null;
5621
- this.suppressNextClick = false;
5622
- this.boundViewportResizeHandler = null;
5623
- this.boundScrollHandler = null;
5624
- }
5625
- static { SpeechOSWidget_1 = this; }
5626
- static { this.styles = [
5627
- themeStyles,
5628
- animations,
5629
- i$4 `
5630
- :host {
5631
- position: fixed;
5632
- bottom: var(--speechos-spacing-md); /* 12px - same as spacing above */
5633
- z-index: var(--speechos-z-base);
5634
- pointer-events: none;
5635
- }
5636
-
5637
- :host {
5638
- left: 50%;
5639
- transform: translateX(-50%);
5640
- }
5683
+ /** Base popup modal styles - simpler than full settings modal */
5684
+ const popupModalStyles = i$4 `
5685
+ :host {
5686
+ position: fixed;
5687
+ inset: 0;
5688
+ pointer-events: none;
5689
+ z-index: calc(var(--speechos-z-base) + 100);
5690
+ }
5641
5691
 
5642
- :host(.custom-position) {
5643
- right: unset;
5644
- left: unset;
5645
- transform: none;
5646
- }
5692
+ .modal-overlay {
5693
+ position: fixed;
5694
+ inset: 0;
5695
+ background: rgba(0, 0, 0, 0.5);
5696
+ display: flex;
5697
+ align-items: center;
5698
+ justify-content: center;
5699
+ z-index: calc(var(--speechos-z-base) + 100);
5700
+ opacity: 0;
5701
+ visibility: hidden;
5702
+ transition: all 0.2s ease;
5703
+ pointer-events: none;
5704
+ backdrop-filter: blur(4px);
5705
+ }
5647
5706
 
5648
- :host(.anchored-to-element) {
5649
- position: absolute;
5707
+ .modal-overlay.open {
5708
+ opacity: 1;
5709
+ visibility: visible;
5710
+ pointer-events: auto;
5711
+ }
5712
+
5713
+ .modal-card {
5714
+ background: #1a1d24;
5715
+ border-radius: 16px;
5716
+ width: 90%;
5717
+ max-width: 400px;
5718
+ display: flex;
5719
+ flex-direction: column;
5720
+ box-shadow: 0 24px 48px rgba(0, 0, 0, 0.4),
5721
+ 0 0 0 1px rgba(255, 255, 255, 0.05);
5722
+ transform: scale(0.95) translateY(10px);
5723
+ transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
5724
+ pointer-events: auto;
5725
+ overflow: hidden;
5726
+ }
5727
+
5728
+ .modal-overlay.open .modal-card {
5729
+ transform: scale(1) translateY(0);
5730
+ }
5731
+
5732
+ .modal-header {
5733
+ display: flex;
5734
+ align-items: center;
5735
+ justify-content: space-between;
5736
+ padding: 16px 20px;
5737
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
5738
+ background: rgba(0, 0, 0, 0.2);
5739
+ }
5740
+
5741
+ .modal-title {
5742
+ font-size: 16px;
5743
+ font-weight: 600;
5744
+ color: white;
5745
+ margin: 0;
5746
+ letter-spacing: -0.01em;
5747
+ }
5748
+
5749
+ .close-button {
5750
+ width: 32px;
5751
+ height: 32px;
5752
+ border-radius: 8px;
5753
+ background: transparent;
5754
+ border: none;
5755
+ cursor: pointer;
5756
+ display: flex;
5757
+ align-items: center;
5758
+ justify-content: center;
5759
+ color: rgba(255, 255, 255, 0.5);
5760
+ transition: all 0.15s ease;
5761
+ }
5762
+
5763
+ .close-button:hover {
5764
+ background: rgba(255, 255, 255, 0.08);
5765
+ color: white;
5766
+ }
5767
+
5768
+ .modal-body {
5769
+ padding: 20px;
5770
+ }
5771
+
5772
+ .modal-footer {
5773
+ display: flex;
5774
+ justify-content: flex-end;
5775
+ gap: 10px;
5776
+ padding: 16px 20px;
5777
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
5778
+ background: rgba(0, 0, 0, 0.1);
5779
+ }
5780
+
5781
+ .btn {
5782
+ padding: 10px 18px;
5783
+ border-radius: 8px;
5784
+ font-size: 13px;
5785
+ font-weight: 600;
5786
+ cursor: pointer;
5787
+ transition: all 0.15s ease;
5788
+ border: none;
5789
+ display: inline-flex;
5790
+ align-items: center;
5791
+ gap: 6px;
5792
+ }
5793
+
5794
+ .btn-secondary {
5795
+ background: rgba(255, 255, 255, 0.08);
5796
+ color: rgba(255, 255, 255, 0.8);
5797
+ }
5798
+
5799
+ .btn-secondary:hover {
5800
+ background: rgba(255, 255, 255, 0.12);
5801
+ color: white;
5802
+ }
5803
+
5804
+ .btn-primary {
5805
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
5806
+ color: white;
5807
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2);
5808
+ }
5809
+
5810
+ .btn-primary:hover {
5811
+ transform: translateY(-1px);
5812
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
5813
+ }
5814
+
5815
+ .btn-primary:active {
5816
+ transform: translateY(0);
5817
+ }
5818
+
5819
+ /* Success state for copy button */
5820
+ .btn-success {
5821
+ background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
5822
+ }
5823
+
5824
+ /* Text display area */
5825
+ .text-display {
5826
+ background: rgba(0, 0, 0, 0.3);
5827
+ border: 1px solid rgba(255, 255, 255, 0.08);
5828
+ border-radius: 10px;
5829
+ padding: 14px 16px;
5830
+ color: white;
5831
+ font-size: 14px;
5832
+ line-height: 1.5;
5833
+ max-height: 200px;
5834
+ overflow-y: auto;
5835
+ white-space: pre-wrap;
5836
+ word-break: break-word;
5837
+ }
5838
+
5839
+ /* Scrollbar styling */
5840
+ .text-display::-webkit-scrollbar {
5841
+ width: 6px;
5842
+ }
5843
+
5844
+ .text-display::-webkit-scrollbar-track {
5845
+ background: transparent;
5846
+ }
5847
+
5848
+ .text-display::-webkit-scrollbar-thumb {
5849
+ background: rgba(255, 255, 255, 0.15);
5850
+ border-radius: 3px;
5851
+ }
5852
+
5853
+ .text-display::-webkit-scrollbar-thumb:hover {
5854
+ background: rgba(255, 255, 255, 0.25);
5855
+ }
5856
+
5857
+ /* Instruction list styling */
5858
+ .instruction-list {
5859
+ list-style: none;
5860
+ padding: 0;
5861
+ margin: 0;
5862
+ }
5863
+
5864
+ .instruction-item {
5865
+ display: flex;
5866
+ align-items: flex-start;
5867
+ gap: 12px;
5868
+ padding: 12px 0;
5869
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
5870
+ }
5871
+
5872
+ .instruction-item:last-child {
5873
+ border-bottom: none;
5874
+ }
5875
+
5876
+ .instruction-number {
5877
+ width: 24px;
5878
+ height: 24px;
5879
+ border-radius: 50%;
5880
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
5881
+ color: white;
5882
+ font-size: 12px;
5883
+ font-weight: 700;
5884
+ display: flex;
5885
+ align-items: center;
5886
+ justify-content: center;
5887
+ flex-shrink: 0;
5888
+ }
5889
+
5890
+ .instruction-text {
5891
+ font-size: 14px;
5892
+ color: rgba(255, 255, 255, 0.85);
5893
+ line-height: 1.5;
5894
+ padding-top: 2px;
5895
+ }
5896
+
5897
+ /* Mobile adjustments */
5898
+ @media (max-width: 480px) {
5899
+ .modal-card {
5900
+ width: 95%;
5901
+ max-width: none;
5902
+ border-radius: 12px;
5903
+ }
5904
+
5905
+ .modal-header {
5906
+ padding: 14px 16px;
5907
+ }
5908
+
5909
+ .modal-body {
5910
+ padding: 16px;
5911
+ }
5912
+
5913
+ .modal-footer {
5914
+ padding: 14px 16px;
5915
+ }
5916
+
5917
+ .modal-title {
5918
+ font-size: 15px;
5919
+ }
5920
+ }
5921
+ `;
5922
+
5923
+ /**
5924
+ * Dictation output modal component
5925
+ * Displays transcribed text with copy functionality when no field is focused
5926
+ */
5927
+ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$1 {
5928
+ constructor() {
5929
+ super(...arguments);
5930
+ this.open = false;
5931
+ this.text = "";
5932
+ this.copied = false;
5933
+ this.copyTimeout = null;
5934
+ }
5935
+ static { this.styles = [
5936
+ themeStyles,
5937
+ popupModalStyles,
5938
+ i$4 `
5939
+ .header-content {
5940
+ display: flex;
5941
+ align-items: center;
5942
+ gap: 12px;
5943
+ }
5944
+
5945
+ .logo-icon {
5946
+ width: 32px;
5947
+ height: 32px;
5948
+ border-radius: 8px;
5949
+ background: linear-gradient(135deg, #10b981 0%, #8b5cf6 100%);
5950
+ display: flex;
5951
+ align-items: center;
5952
+ justify-content: center;
5953
+ flex-shrink: 0;
5954
+ }
5955
+
5956
+ .logo-icon svg {
5957
+ width: 18px;
5958
+ height: 18px;
5959
+ color: white;
5960
+ }
5961
+
5962
+ .modal-title {
5963
+ background: linear-gradient(135deg, #34d399 0%, #a78bfa 100%);
5964
+ -webkit-background-clip: text;
5965
+ -webkit-text-fill-color: transparent;
5966
+ background-clip: text;
5967
+ }
5968
+
5969
+ .btn-primary {
5970
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
5971
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
5972
+ border-radius: 999px;
5973
+ }
5974
+
5975
+ .btn-primary:hover {
5976
+ background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
5977
+ transform: translateY(-2px);
5978
+ box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
5979
+ }
5980
+
5981
+ .btn-primary:active {
5982
+ transform: translateY(0);
5983
+ }
5984
+
5985
+ .btn-success {
5986
+ background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
5987
+ box-shadow: 0 4px 12px rgba(52, 211, 153, 0.3);
5988
+ border-radius: 999px;
5989
+ }
5990
+
5991
+ .btn-secondary {
5992
+ border-radius: 999px;
5993
+ }
5994
+
5995
+ .hint {
5996
+ display: flex;
5997
+ align-items: center;
5998
+ gap: 6px;
5999
+ margin-top: 12px;
6000
+ padding: 8px 12px;
6001
+ background: rgba(16, 185, 129, 0.08);
6002
+ border-radius: 8px;
6003
+ font-size: 12px;
6004
+ color: rgba(255, 255, 255, 0.6);
6005
+ }
6006
+
6007
+ .hint-icon {
6008
+ color: #10b981;
6009
+ flex-shrink: 0;
6010
+ }
6011
+ `,
6012
+ ]; }
6013
+ disconnectedCallback() {
6014
+ super.disconnectedCallback();
6015
+ if (this.copyTimeout) {
6016
+ clearTimeout(this.copyTimeout);
6017
+ this.copyTimeout = null;
6018
+ }
6019
+ }
6020
+ updated(changedProperties) {
6021
+ if (changedProperties.has("open")) {
6022
+ if (!this.open) {
6023
+ // Reset copied state when modal closes
6024
+ this.copied = false;
6025
+ if (this.copyTimeout) {
6026
+ clearTimeout(this.copyTimeout);
6027
+ this.copyTimeout = null;
6028
+ }
6029
+ }
6030
+ }
6031
+ }
6032
+ handleOverlayClick(e) {
6033
+ if (e.target === e.currentTarget) {
6034
+ this.close();
6035
+ }
6036
+ }
6037
+ handleClose() {
6038
+ this.close();
6039
+ }
6040
+ close() {
6041
+ this.dispatchEvent(new CustomEvent("modal-close", {
6042
+ bubbles: true,
6043
+ composed: true,
6044
+ }));
6045
+ }
6046
+ async handleCopy() {
6047
+ try {
6048
+ await navigator.clipboard.writeText(this.text);
6049
+ this.copied = true;
6050
+ // Reset copied state after 2 seconds
6051
+ if (this.copyTimeout) {
6052
+ clearTimeout(this.copyTimeout);
6053
+ }
6054
+ this.copyTimeout = window.setTimeout(() => {
6055
+ this.copied = false;
6056
+ this.copyTimeout = null;
6057
+ }, 2000);
6058
+ }
6059
+ catch (err) {
6060
+ console.error("[SpeechOS] Failed to copy text:", err);
6061
+ }
6062
+ }
6063
+ render() {
6064
+ return b `
6065
+ <div
6066
+ class="modal-overlay ${this.open ? "open" : ""}"
6067
+ @click="${this.handleOverlayClick}"
6068
+ >
6069
+ <div class="modal-card">
6070
+ <div class="modal-header">
6071
+ <div class="header-content">
6072
+ <div class="logo-icon">${micIcon(18)}</div>
6073
+ <h2 class="modal-title">Dictation Complete</h2>
6074
+ </div>
6075
+ <button
6076
+ class="close-button"
6077
+ @click="${this.handleClose}"
6078
+ aria-label="Close"
6079
+ >
6080
+ ${xIcon(16)}
6081
+ </button>
6082
+ </div>
6083
+
6084
+ <div class="modal-body">
6085
+ <div class="text-display">${this.text}</div>
6086
+ <div class="hint">
6087
+ <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">
6088
+ <circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
6089
+ </svg>
6090
+ <span>Tip: Focus a text field first to auto-insert next time</span>
6091
+ </div>
6092
+ </div>
6093
+
6094
+ <div class="modal-footer">
6095
+ <button
6096
+ class="btn ${this.copied ? "btn-success" : "btn-primary"}"
6097
+ @click="${this.handleCopy}"
6098
+ >
6099
+ ${this.copied ? checkIcon(16) : copyIcon(16)}
6100
+ ${this.copied ? "Copied!" : "Copy"}
6101
+ </button>
6102
+ <button class="btn btn-secondary" @click="${this.handleClose}">
6103
+ Done
6104
+ </button>
6105
+ </div>
6106
+ </div>
6107
+ </div>
6108
+ `;
6109
+ }
6110
+ };
6111
+ __decorate([
6112
+ n({ type: Boolean })
6113
+ ], SpeechOSDictationOutputModal.prototype, "open", void 0);
6114
+ __decorate([
6115
+ n({ type: String })
6116
+ ], SpeechOSDictationOutputModal.prototype, "text", void 0);
6117
+ __decorate([
6118
+ r()
6119
+ ], SpeechOSDictationOutputModal.prototype, "copied", void 0);
6120
+ SpeechOSDictationOutputModal = __decorate([
6121
+ t$1("speechos-dictation-output-modal")
6122
+ ], SpeechOSDictationOutputModal);
6123
+
6124
+ /**
6125
+ * Edit help modal component
6126
+ * Displays usage instructions for the edit feature when no text is selected
6127
+ */
6128
+ let SpeechOSEditHelpModal = class SpeechOSEditHelpModal extends i$1 {
6129
+ constructor() {
6130
+ super(...arguments);
6131
+ this.open = false;
6132
+ }
6133
+ static { this.styles = [
6134
+ themeStyles,
6135
+ popupModalStyles,
6136
+ i$4 `
6137
+ .header-content {
6138
+ display: flex;
6139
+ align-items: center;
6140
+ gap: 12px;
6141
+ }
6142
+
6143
+ .logo-icon {
6144
+ width: 32px;
6145
+ height: 32px;
6146
+ border-radius: 8px;
6147
+ background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
6148
+ display: flex;
6149
+ align-items: center;
6150
+ justify-content: center;
6151
+ flex-shrink: 0;
6152
+ }
6153
+
6154
+ .logo-icon svg {
6155
+ width: 18px;
6156
+ height: 18px;
6157
+ color: white;
6158
+ }
6159
+
6160
+ .modal-title {
6161
+ background: linear-gradient(135deg, #a78bfa 0%, #818cf8 100%);
6162
+ -webkit-background-clip: text;
6163
+ -webkit-text-fill-color: transparent;
6164
+ background-clip: text;
6165
+ }
6166
+
6167
+ .instruction-number {
6168
+ background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
6169
+ }
6170
+
6171
+ .btn-primary {
6172
+ background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
6173
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
6174
+ border-radius: 999px;
6175
+ }
6176
+
6177
+ .btn-primary:hover {
6178
+ background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
6179
+ transform: translateY(-2px);
6180
+ box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
6181
+ }
6182
+
6183
+ .btn-primary:active {
6184
+ transform: translateY(0);
6185
+ }
6186
+ `,
6187
+ ]; }
6188
+ handleOverlayClick(e) {
6189
+ if (e.target === e.currentTarget) {
6190
+ this.close();
6191
+ }
6192
+ }
6193
+ handleClose() {
6194
+ this.close();
6195
+ }
6196
+ close() {
6197
+ this.dispatchEvent(new CustomEvent("modal-close", {
6198
+ bubbles: true,
6199
+ composed: true,
6200
+ }));
6201
+ }
6202
+ render() {
6203
+ return b `
6204
+ <div
6205
+ class="modal-overlay ${this.open ? "open" : ""}"
6206
+ @click="${this.handleOverlayClick}"
6207
+ >
6208
+ <div class="modal-card">
6209
+ <div class="modal-header">
6210
+ <div class="header-content">
6211
+ <div class="logo-icon">${editIcon(18)}</div>
6212
+ <h2 class="modal-title">How to Use Edit</h2>
6213
+ </div>
6214
+ <button
6215
+ class="close-button"
6216
+ @click="${this.handleClose}"
6217
+ aria-label="Close"
6218
+ >
6219
+ ${xIcon(16)}
6220
+ </button>
6221
+ </div>
6222
+
6223
+ <div class="modal-body">
6224
+ <ol class="instruction-list">
6225
+ <li class="instruction-item">
6226
+ <span class="instruction-number">1</span>
6227
+ <span class="instruction-text">
6228
+ Click on a text field to focus it, or select the text you want
6229
+ to edit
6230
+ </span>
6231
+ </li>
6232
+ <li class="instruction-item">
6233
+ <span class="instruction-number">2</span>
6234
+ <span class="instruction-text">
6235
+ Click the Edit button in the SpeechOS widget
6236
+ </span>
6237
+ </li>
6238
+ <li class="instruction-item">
6239
+ <span class="instruction-number">3</span>
6240
+ <span class="instruction-text">
6241
+ Speak your editing instructions (e.g., "make it more formal"
6242
+ or "fix the grammar")
6243
+ </span>
6244
+ </li>
6245
+ </ol>
6246
+ </div>
6247
+
6248
+ <div class="modal-footer">
6249
+ <button class="btn btn-primary" @click="${this.handleClose}">
6250
+ Got it
6251
+ </button>
6252
+ </div>
6253
+ </div>
6254
+ </div>
6255
+ `;
6256
+ }
6257
+ };
6258
+ __decorate([
6259
+ n({ type: Boolean })
6260
+ ], SpeechOSEditHelpModal.prototype, "open", void 0);
6261
+ SpeechOSEditHelpModal = __decorate([
6262
+ t$1("speechos-edit-help-modal")
6263
+ ], SpeechOSEditHelpModal);
6264
+
6265
+ /**
6266
+ * Main widget container component
6267
+ * Composes mic button and action bubbles with state management
6268
+ */
6269
+ var SpeechOSWidget_1;
6270
+ /**
6271
+ * Minimum duration to show the connecting animation (in milliseconds).
6272
+ * Since mic capture starts almost instantly, we enforce a minimum animation
6273
+ * duration so users can see the visual feedback before transitioning to recording.
6274
+ */
6275
+ const MIN_CONNECTING_ANIMATION_MS = 200;
6276
+ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6277
+ constructor() {
6278
+ super(...arguments);
6279
+ this.widgetState = state.getState();
6280
+ this.settingsOpen = false;
6281
+ this.dictationModalOpen = false;
6282
+ this.dictationModalText = "";
6283
+ this.editHelpModalOpen = false;
6284
+ this.commandFeedback = null;
6285
+ this.dictationTargetElement = null;
6286
+ this.editTargetElement = null;
6287
+ this.dictationCursorStart = null;
6288
+ this.dictationCursorEnd = null;
6289
+ this.editSelectionStart = null;
6290
+ this.editSelectionEnd = null;
6291
+ this.editSelectedText = "";
6292
+ this.boundClickOutsideHandler = null;
6293
+ this.modalElement = null;
6294
+ this.dictationModalElement = null;
6295
+ this.editHelpModalElement = null;
6296
+ this.commandFeedbackTimeout = null;
6297
+ this.customPosition = null;
6298
+ this.isDragging = false;
6299
+ this.dragStartPos = null;
6300
+ this.dragOffset = { x: 0, y: 0 };
6301
+ this.boundDragMove = null;
6302
+ this.boundDragEnd = null;
6303
+ this.suppressNextClick = false;
6304
+ this.boundViewportResizeHandler = null;
6305
+ this.boundScrollHandler = null;
6306
+ }
6307
+ static { SpeechOSWidget_1 = this; }
6308
+ static { this.styles = [
6309
+ themeStyles,
6310
+ animations,
6311
+ i$4 `
6312
+ :host {
6313
+ position: fixed;
6314
+ bottom: var(--speechos-spacing-md); /* 12px - same as spacing above */
6315
+ z-index: var(--speechos-z-base);
6316
+ pointer-events: none;
6317
+ }
6318
+
6319
+ :host {
6320
+ left: 50%;
6321
+ transform: translateX(-50%);
6322
+ }
6323
+
6324
+ :host(.custom-position) {
6325
+ right: unset;
6326
+ left: unset;
6327
+ transform: none;
6328
+ }
6329
+
6330
+ :host(.anchored-to-element) {
6331
+ position: absolute;
5650
6332
  bottom: unset;
5651
6333
  right: unset;
5652
6334
  left: unset;
@@ -5705,6 +6387,18 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
5705
6387
  this.settingsOpen = false;
5706
6388
  });
5707
6389
  document.body.appendChild(this.modalElement);
6390
+ // Mount dictation output modal
6391
+ this.dictationModalElement = document.createElement("speechos-dictation-output-modal");
6392
+ this.dictationModalElement.addEventListener("modal-close", () => {
6393
+ this.dictationModalOpen = false;
6394
+ });
6395
+ document.body.appendChild(this.dictationModalElement);
6396
+ // Mount edit help modal
6397
+ this.editHelpModalElement = document.createElement("speechos-edit-help-modal");
6398
+ this.editHelpModalElement.addEventListener("modal-close", () => {
6399
+ this.editHelpModalOpen = false;
6400
+ });
6401
+ document.body.appendChild(this.editHelpModalElement);
5708
6402
  this.stateUnsubscribe = state.subscribe((newState) => {
5709
6403
  if (!newState.isVisible || !newState.isExpanded) {
5710
6404
  this.settingsOpen = false;
@@ -5744,6 +6438,18 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
5744
6438
  this.modalElement.remove();
5745
6439
  this.modalElement = null;
5746
6440
  }
6441
+ if (this.dictationModalElement) {
6442
+ this.dictationModalElement.remove();
6443
+ this.dictationModalElement = null;
6444
+ }
6445
+ if (this.editHelpModalElement) {
6446
+ this.editHelpModalElement.remove();
6447
+ this.editHelpModalElement = null;
6448
+ }
6449
+ if (this.commandFeedbackTimeout) {
6450
+ clearTimeout(this.commandFeedbackTimeout);
6451
+ this.commandFeedbackTimeout = null;
6452
+ }
5747
6453
  if (this.stateUnsubscribe) {
5748
6454
  this.stateUnsubscribe();
5749
6455
  }
@@ -5775,6 +6481,15 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
5775
6481
  if (changedProperties.has("settingsOpen") && this.modalElement) {
5776
6482
  this.modalElement.open = this.settingsOpen;
5777
6483
  }
6484
+ if (changedProperties.has("dictationModalOpen") && this.dictationModalElement) {
6485
+ this.dictationModalElement.open = this.dictationModalOpen;
6486
+ }
6487
+ if (changedProperties.has("dictationModalText") && this.dictationModalElement) {
6488
+ this.dictationModalElement.text = this.dictationModalText;
6489
+ }
6490
+ if (changedProperties.has("editHelpModalOpen") && this.editHelpModalElement) {
6491
+ this.editHelpModalElement.open = this.editHelpModalOpen;
6492
+ }
5778
6493
  }
5779
6494
  handleClickOutside(event) {
5780
6495
  const target = event.target;
@@ -5813,7 +6528,10 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
5813
6528
  }
5814
6529
  if (!clickedInWidget) {
5815
6530
  getBackend().stopAutoRefresh?.();
5816
- state.hide();
6531
+ // Don't hide if alwaysVisible is enabled
6532
+ if (!isAlwaysVisible()) {
6533
+ state.hide();
6534
+ }
5817
6535
  }
5818
6536
  }
5819
6537
  isFormField(element) {
@@ -5934,6 +6652,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
5934
6652
  return;
5935
6653
  }
5936
6654
  if (this.widgetState.recordingState === "idle") {
6655
+ // Clear command feedback on any mic click
6656
+ this.clearCommandFeedback();
5937
6657
  // If we're expanding, prefetch the token to reduce latency when user selects an action
5938
6658
  if (!this.widgetState.isExpanded) {
5939
6659
  // Fire and forget - we don't need to wait for this (LiveKit only)
@@ -5962,12 +6682,19 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
5962
6682
  }
5963
6683
  else {
5964
6684
  state.stopRecording();
5965
- getConfig();
5966
6685
  const backend = getBackend();
5967
6686
  try {
5968
6687
  const transcription = await this.withMinDisplayTime(backend.stopVoiceSession(), 300);
5969
6688
  if (transcription) {
5970
- this.insertTranscription(transcription);
6689
+ // Check if we have a target element to insert into
6690
+ if (this.dictationTargetElement) {
6691
+ this.insertTranscription(transcription);
6692
+ }
6693
+ else {
6694
+ // No target element - show dictation output modal
6695
+ this.dictationModalText = transcription;
6696
+ this.dictationModalOpen = true;
6697
+ }
5971
6698
  transcriptStore.saveTranscript(transcription, "dictate");
5972
6699
  }
5973
6700
  state.completeRecording();
@@ -6015,6 +6742,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6015
6742
  }
6016
6743
  }
6017
6744
  handleCloseWidget() {
6745
+ this.clearCommandFeedback();
6018
6746
  getBackend().stopAutoRefresh?.();
6019
6747
  state.hide();
6020
6748
  }
@@ -6142,11 +6870,20 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6142
6870
  }
6143
6871
  handleActionSelect(event) {
6144
6872
  const { action } = event.detail;
6873
+ // Clear any existing command feedback when a new action is selected
6874
+ this.clearCommandFeedback();
6145
6875
  state.setActiveAction(action);
6146
6876
  if (action === "dictate") {
6147
6877
  this.startDictation();
6148
6878
  }
6149
6879
  else if (action === "edit") {
6880
+ // Check if there's a focused element before starting edit
6881
+ if (!this.widgetState.focusedElement) {
6882
+ // No focused element - show edit help modal
6883
+ this.editHelpModalOpen = true;
6884
+ state.setActiveAction(null);
6885
+ return;
6886
+ }
6150
6887
  this.startEdit();
6151
6888
  }
6152
6889
  else if (action === "command") {
@@ -6353,6 +7090,10 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6353
7090
  // Note: command:complete event is already emitted by the backend
6354
7091
  // when the command_result message is received, so we don't emit here
6355
7092
  state.completeRecording();
7093
+ // Keep widget visible but collapsed (just mic button, no action bubbles)
7094
+ state.setState({ isExpanded: false });
7095
+ // Show command feedback
7096
+ this.showCommandFeedback(result ? "success" : "none");
6356
7097
  backend.disconnect().catch(() => { });
6357
7098
  // Start auto-refresh to keep token fresh for subsequent commands (LiveKit only)
6358
7099
  backend.startAutoRefresh?.();
@@ -6365,6 +7106,25 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6365
7106
  }
6366
7107
  }
6367
7108
  }
7109
+ showCommandFeedback(feedback) {
7110
+ this.commandFeedback = feedback;
7111
+ // Clear any existing timeout
7112
+ if (this.commandFeedbackTimeout) {
7113
+ clearTimeout(this.commandFeedbackTimeout);
7114
+ }
7115
+ // Auto-dismiss after 4 seconds
7116
+ this.commandFeedbackTimeout = window.setTimeout(() => {
7117
+ this.commandFeedback = null;
7118
+ this.commandFeedbackTimeout = null;
7119
+ }, 4000);
7120
+ }
7121
+ clearCommandFeedback() {
7122
+ if (this.commandFeedbackTimeout) {
7123
+ clearTimeout(this.commandFeedbackTimeout);
7124
+ this.commandFeedbackTimeout = null;
7125
+ }
7126
+ this.commandFeedback = null;
7127
+ }
6368
7128
  supportsSelection(element) {
6369
7129
  if (element.tagName.toLowerCase() === "textarea") {
6370
7130
  return true;
@@ -6482,6 +7242,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6482
7242
  activeAction="${this.widgetState.activeAction || ""}"
6483
7243
  editPreviewText="${this.editSelectedText}"
6484
7244
  errorMessage="${this.widgetState.errorMessage || ""}"
7245
+ .commandFeedback="${this.commandFeedback}"
6485
7246
  @mic-click="${this.handleMicClick}"
6486
7247
  @stop-recording="${this.handleStopRecording}"
6487
7248
  @cancel-operation="${this.handleCancelOperation}"
@@ -6499,6 +7260,18 @@ __decorate([
6499
7260
  __decorate([
6500
7261
  r()
6501
7262
  ], SpeechOSWidget.prototype, "settingsOpen", void 0);
7263
+ __decorate([
7264
+ r()
7265
+ ], SpeechOSWidget.prototype, "dictationModalOpen", void 0);
7266
+ __decorate([
7267
+ r()
7268
+ ], SpeechOSWidget.prototype, "dictationModalText", void 0);
7269
+ __decorate([
7270
+ r()
7271
+ ], SpeechOSWidget.prototype, "editHelpModalOpen", void 0);
7272
+ __decorate([
7273
+ r()
7274
+ ], SpeechOSWidget.prototype, "commandFeedback", void 0);
6502
7275
  SpeechOSWidget = SpeechOSWidget_1 = __decorate([
6503
7276
  t$1("speechos-widget")
6504
7277
  ], SpeechOSWidget);
@@ -6579,6 +7352,10 @@ class SpeechOS {
6579
7352
  }
6580
7353
  // Create and mount widget
6581
7354
  this.mountWidget();
7355
+ // If alwaysVisible is enabled, show the widget immediately
7356
+ if (isAlwaysVisible()) {
7357
+ state.show();
7358
+ }
6582
7359
  this.isInitialized = true;
6583
7360
  // Log initialization in debug mode
6584
7361
  if (finalConfig.debug) {