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