@myscheme/voice-navigation-sdk 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -11,7 +11,9 @@ A TypeScript SDK for voice-controlled navigation using Azure Speech-to-Text and
11
11
  - 🖱️ **Rich browser and media controls**
12
12
  - ♿ **Accessibility-first design**
13
13
  - 🎨 **Customizable floating UI** control
14
- - 📦 **Full TypeScript support**
14
+ - **Flexible button options** - use default or your own custom button
15
+ - 📍 **Configurable button placement** - position the default button anywhere
16
+ - �📦 **Full TypeScript support**
15
17
  - 🔄 **Reusable across multiple websites**
16
18
  - 🔍 **Vector search integration** with OpenSearch
17
19
 
@@ -94,6 +96,7 @@ const controller = initNavigationOnMicrophone({
94
96
  | `actionHandlers` | `object` | ❌ | Custom action callbacks |
95
97
  | `pages` | `object` | ❌ | Dynamic page navigation configuration |
96
98
  | `opensearch` | `object` | ❌ | OpenSearch vector search configuration |
99
+ | `ui` | `object` | ❌ | UI customization (button placement & style) |
97
100
 
98
101
  ### OpenSearch Configuration
99
102
 
@@ -112,6 +115,252 @@ When providing the optional `opensearch` block:
112
115
  | `sourceFields` | `string[]` | ❌ | Source fields to retrieve |
113
116
  | `apiPath` | `string` | ❌ | Proxy endpoint (default: `/api/voice-navigation/vector-search`) |
114
117
 
118
+ ### UI Configuration
119
+
120
+ The library provides flexible button options - use the default floating button or integrate with your own custom button.
121
+
122
+ #### Configuration Options
123
+
124
+ | Property | Type | Required | Description |
125
+ | ---------------------- | ----------------- | -------- | ------------------------------------------------------------- |
126
+ | `showDefaultButton` | `boolean` | ❌ | Show library's default button (default: `true`) |
127
+ | `customButtonSelector` | `string` | ❌ | CSS selector for your custom button (e.g., `"#my-voice-btn"`) |
128
+ | `buttonPlacement` | `ButtonPlacement` | ❌ | Position of default button (default: `"bottom-right"`) |
129
+
130
+ **ButtonPlacement** options: `"bottom-right"` \| `"bottom-left"` \| `"top-right"` \| `"top-left"` \| `"center-right"` \| `"center-left"`
131
+
132
+ #### Option 1: Default Button with Custom Placement
133
+
134
+ Use the library's pre-styled floating button at different positions:
135
+
136
+ ```typescript
137
+ const controller = initNavigationOnMicrophone({
138
+ azure: {
139
+ /* ... */
140
+ },
141
+ aws: {
142
+ /* ... */
143
+ },
144
+
145
+ // Place button at top-left corner
146
+ ui: {
147
+ showDefaultButton: true,
148
+ buttonPlacement: "top-left",
149
+ },
150
+ });
151
+ ```
152
+
153
+ **Available placements:**
154
+
155
+ - `"bottom-right"` (default) - Bottom right corner
156
+ - `"bottom-left"` - Bottom left corner
157
+ - `"top-right"` - Top right corner
158
+ - `"top-left"` - Top left corner
159
+ - `"center-right"` - Vertically centered on right
160
+ - `"center-left"` - Vertically centered on left
161
+
162
+ #### Option 2: Custom Button
163
+
164
+ Use your own button design and have the library attach voice control functionality to it:
165
+
166
+ **1. Create your button in HTML:**
167
+
168
+ ```html
169
+ <button id="my-voice-button" class="my-custom-style">
170
+ <span>🎤 Voice Control</span>
171
+ </button>
172
+ ```
173
+
174
+ **2. Configure the SDK to use your button:**
175
+
176
+ ```typescript
177
+ const controller = initNavigationOnMicrophone({
178
+ azure: {
179
+ /* ... */
180
+ },
181
+ aws: {
182
+ /* ... */
183
+ },
184
+
185
+ ui: {
186
+ showDefaultButton: false,
187
+ customButtonSelector: "#my-voice-button",
188
+ },
189
+ });
190
+ ```
191
+
192
+ **Important Notes for Custom Buttons:**
193
+
194
+ - **Button must exist in DOM before SDK initialization** - Add the SDK initialization code after your button is rendered, or use `DOMContentLoaded` event
195
+ - The library will automatically add `aria-pressed` and `aria-label` attributes for accessibility
196
+ - The button can be any clickable element (button, div, etc.)
197
+ - The library handles all click events and state management
198
+ - Visual feedback panel will display near the button when voice control is active
199
+ - You are responsible for styling the button (pressed, disabled, etc.) based on your design requirements
200
+
201
+ **React Example with Custom Button:**
202
+
203
+ ```tsx
204
+ import { useEffect, useRef } from "react";
205
+ import { initNavigationOnMicrophone } from "@myscheme/voice-navigation-sdk";
206
+
207
+ export default function VoiceNavigationProvider() {
208
+ const controllerRef = useRef(null);
209
+
210
+ useEffect(() => {
211
+ // Initialize after button is rendered
212
+ controllerRef.current = initNavigationOnMicrophone({
213
+ azure: {
214
+ subscriptionKey: process.env.NEXT_PUBLIC_AZURE_SPEECH_KEY!,
215
+ region: process.env.NEXT_PUBLIC_AZURE_SPEECH_REGION!,
216
+ },
217
+ aws: {
218
+ accessKeyId: process.env.NEXT_PUBLIC_AWS_ACCESS_KEY_ID!,
219
+ secretAccessKey: process.env.NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY!,
220
+ modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
221
+ region: "ap-south-1",
222
+ },
223
+ ui: {
224
+ showDefaultButton: false,
225
+ customButtonSelector: "#voice-control-btn",
226
+ },
227
+ });
228
+
229
+ return () => controllerRef.current?.destroy();
230
+ }, []);
231
+
232
+ return (
233
+ <button
234
+ id="voice-control-btn"
235
+ className="px-4 py-2 bg-blue-600 text-white rounded-full hover:bg-blue-700"
236
+ >
237
+ 🎤 Voice Control
238
+ </button>
239
+ );
240
+ }
241
+ ```
242
+
243
+ **Next.js App Router Example:**
244
+
245
+ ```tsx
246
+ // app/components/voice-button.tsx
247
+ "use client";
248
+
249
+ export default function VoiceButton() {
250
+ return (
251
+ <button
252
+ id="voice-nav-button"
253
+ className="fixed bottom-4 right-4 w-16 h-16 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg hover:scale-110 transition-transform"
254
+ aria-label="Activate voice navigation"
255
+ >
256
+ 🎤
257
+ </button>
258
+ );
259
+ }
260
+
261
+ // app/providers/voice-navigation.tsx
262
+ ("use client");
263
+
264
+ import { useEffect } from "react";
265
+ import { initNavigationOnMicrophone } from "@myscheme/voice-navigation-sdk";
266
+
267
+ export default function VoiceNavigationProvider() {
268
+ useEffect(() => {
269
+ const controller = initNavigationOnMicrophone({
270
+ azure: {
271
+ /* ... */
272
+ },
273
+ aws: {
274
+ /* ... */
275
+ },
276
+ ui: {
277
+ showDefaultButton: false,
278
+ customButtonSelector: "#voice-nav-button",
279
+ },
280
+ });
281
+
282
+ return () => controller.destroy();
283
+ }, []);
284
+
285
+ return null;
286
+ }
287
+
288
+ // app/layout.tsx
289
+ import VoiceButton from "./components/voice-button";
290
+ import VoiceNavigationProvider from "./providers/voice-navigation";
291
+
292
+ export default function RootLayout({ children }) {
293
+ return (
294
+ <html>
295
+ <body>
296
+ <VoiceButton />
297
+ <VoiceNavigationProvider />
298
+ {children}
299
+ </body>
300
+ </html>
301
+ );
302
+ }
303
+ ```
304
+
305
+ **Vanilla JavaScript Example:**
306
+
307
+ ```html
308
+ <!DOCTYPE html>
309
+ <html>
310
+ <head>
311
+ <style>
312
+ .voice-btn {
313
+ position: fixed;
314
+ bottom: 20px;
315
+ right: 20px;
316
+ width: 60px;
317
+ height: 60px;
318
+ border-radius: 50%;
319
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
320
+ color: white;
321
+ border: none;
322
+ cursor: pointer;
323
+ font-size: 24px;
324
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
325
+ transition: transform 0.2s;
326
+ }
327
+
328
+ .voice-btn:hover {
329
+ transform: scale(1.1);
330
+ }
331
+
332
+ .voice-btn:active {
333
+ transform: scale(0.95);
334
+ }
335
+ </style>
336
+ </head>
337
+ <body>
338
+ <button id="voice-control" class="voice-btn">🎤</button>
339
+
340
+ <script type="module">
341
+ import { initNavigationOnMicrophone } from "@myscheme/voice-navigation-sdk";
342
+
343
+ const controller = initNavigationOnMicrophone({
344
+ azure: {
345
+ subscriptionKey: "your-key",
346
+ region: "your-region",
347
+ },
348
+ aws: {
349
+ accessKeyId: "your-key",
350
+ secretAccessKey: "your-secret",
351
+ modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
352
+ region: "ap-south-1",
353
+ },
354
+ ui: {
355
+ showDefaultButton: false,
356
+ customButtonSelector: "#voice-control",
357
+ },
358
+ });
359
+ </script>
360
+ </body>
361
+ </html>
362
+ ```
363
+
115
364
  ## 🌐 Dynamic Page Navigation
116
365
 
117
366
  Instead of hardcoding page navigation, configure pages dynamically using XML or direct configuration.
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAEhB,aAAa,EACb,YAAY,EACZ,mBAAmB,EACpB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAsChE,eAAO,MAAM,eAAe,GAAI,UAAU,YAAY,KAAG,IAOxD,CAAC;AAKF,eAAO,MAAM,eAAe,QAAO,YAAY,GAAG,IAEjD,CAAC;AAkCF,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,MAS5C,CAAC;AAKF,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,MAM1C,CAAC;AAKF,eAAO,MAAM,iBAAiB,GAAI,QAAQ,MAAM,KAAG,MAElD,CAAC;AAKF,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,gBAAgB,EACxB,UAAS,aAAkB,KAC1B,YA4VF,CAAC;AAKF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,KAAG,mBAAmB,GAAG,IAiCtE,CAAC;AAKF,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,KAAG,MAAM,IAAI,gBAY1D,CAAC"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAEhB,aAAa,EACb,YAAY,EACZ,mBAAmB,EACpB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAyDhE,eAAO,MAAM,eAAe,GAAI,UAAU,YAAY,KAAG,IAOxD,CAAC;AAKF,eAAO,MAAM,eAAe,QAAO,YAAY,GAAG,IAEjD,CAAC;AAkCF,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,MAS5C,CAAC;AAKF,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,MAM1C,CAAC;AAKF,eAAO,MAAM,iBAAiB,GAAI,QAAQ,MAAM,KAAG,MAElD,CAAC;AAKF,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,gBAAgB,EACxB,UAAS,aAAkB,KAC1B,YA4yBF,CAAC;AAKF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,KAAG,mBAAmB,GAAG,IAiCtE,CAAC;AAKF,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,KAAG,MAAM,IAAI,gBAY1D,CAAC"}
package/dist/actions.js CHANGED
@@ -25,6 +25,25 @@ const CORE_ACTIONS = new Set([
25
25
  "unmute_media",
26
26
  "search_content",
27
27
  "stop",
28
+ "quit",
29
+ "tab_next",
30
+ "tab_back",
31
+ "click",
32
+ "enter",
33
+ "submit",
34
+ "move_left",
35
+ "move_right",
36
+ "move_up",
37
+ "move_down",
38
+ "move_beginning",
39
+ "move_end",
40
+ "select_all",
41
+ "select_line",
42
+ "type_text",
43
+ "cut",
44
+ "copy",
45
+ "paste",
46
+ "clear",
28
47
  "unknown",
29
48
  ]);
30
49
  let globalPageRegistry = null;
@@ -351,6 +370,382 @@ export const performAgentAction = (action, context = {}) => {
351
370
  performed = true;
352
371
  break;
353
372
  }
373
+ case "quit": {
374
+ console.log("Quit command received");
375
+ if (onStop) {
376
+ onStop();
377
+ }
378
+ info.quit = true;
379
+ performed = true;
380
+ break;
381
+ }
382
+ case "tab_next": {
383
+ const focusableElements = Array.from(document.querySelectorAll('a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])')).filter((el) => {
384
+ const rect = el.getBoundingClientRect();
385
+ return (!el.hasAttribute("disabled") &&
386
+ rect.width > 0 &&
387
+ rect.height > 0 &&
388
+ window.getComputedStyle(el).visibility !== "hidden");
389
+ });
390
+ const currentIndex = focusableElements.indexOf(document.activeElement);
391
+ const nextIndex = (currentIndex + 1) % focusableElements.length;
392
+ if (focusableElements[nextIndex]) {
393
+ focusableElements[nextIndex].focus();
394
+ console.log("Moved focus to next element");
395
+ info.focused = true;
396
+ info.direction = "next";
397
+ performed = true;
398
+ }
399
+ break;
400
+ }
401
+ case "tab_back": {
402
+ const focusableElements = Array.from(document.querySelectorAll('a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])')).filter((el) => {
403
+ const rect = el.getBoundingClientRect();
404
+ return (!el.hasAttribute("disabled") &&
405
+ rect.width > 0 &&
406
+ rect.height > 0 &&
407
+ window.getComputedStyle(el).visibility !== "hidden");
408
+ });
409
+ const currentIndex = focusableElements.indexOf(document.activeElement);
410
+ const prevIndex = currentIndex <= 0 ? focusableElements.length - 1 : currentIndex - 1;
411
+ if (focusableElements[prevIndex]) {
412
+ focusableElements[prevIndex].focus();
413
+ console.log("Moved focus to previous element");
414
+ info.focused = true;
415
+ info.direction = "back";
416
+ performed = true;
417
+ }
418
+ break;
419
+ }
420
+ case "click":
421
+ case "enter":
422
+ case "submit": {
423
+ const activeElement = document.activeElement;
424
+ if (activeElement && activeElement !== document.body) {
425
+ activeElement.click();
426
+ console.log(`Clicked on focused element: ${activeElement.tagName}`);
427
+ info.clicked = true;
428
+ performed = true;
429
+ }
430
+ else {
431
+ console.log("No element is currently focused");
432
+ }
433
+ break;
434
+ }
435
+ case "move_left": {
436
+ const activeElement = document.activeElement;
437
+ if (activeElement &&
438
+ (activeElement instanceof HTMLInputElement ||
439
+ activeElement instanceof HTMLTextAreaElement)) {
440
+ const currentPos = activeElement.selectionStart || 0;
441
+ activeElement.setSelectionRange(currentPos - 1, currentPos - 1);
442
+ console.log("Moved cursor left");
443
+ info.cursorMoved = true;
444
+ info.direction = "left";
445
+ performed = true;
446
+ }
447
+ break;
448
+ }
449
+ case "move_right": {
450
+ const activeElement = document.activeElement;
451
+ if (activeElement &&
452
+ (activeElement instanceof HTMLInputElement ||
453
+ activeElement instanceof HTMLTextAreaElement)) {
454
+ const currentPos = activeElement.selectionStart || 0;
455
+ activeElement.setSelectionRange(currentPos + 1, currentPos + 1);
456
+ console.log("Moved cursor right");
457
+ info.cursorMoved = true;
458
+ info.direction = "right";
459
+ performed = true;
460
+ }
461
+ break;
462
+ }
463
+ case "move_up": {
464
+ const activeElement = document.activeElement;
465
+ if (activeElement instanceof HTMLTextAreaElement) {
466
+ const lines = activeElement.value.split("\n");
467
+ const currentPos = activeElement.selectionStart || 0;
468
+ let lineStart = 0;
469
+ let currentLine = 0;
470
+ for (let i = 0; i < lines.length; i++) {
471
+ if (currentPos <= lineStart + lines[i].length) {
472
+ currentLine = i;
473
+ break;
474
+ }
475
+ lineStart += lines[i].length + 1;
476
+ }
477
+ if (currentLine > 0) {
478
+ const posInLine = currentPos - lineStart;
479
+ const prevLineStart = lineStart - lines[currentLine - 1].length - 1;
480
+ const newPos = Math.min(prevLineStart + posInLine, prevLineStart + lines[currentLine - 1].length);
481
+ activeElement.setSelectionRange(newPos, newPos);
482
+ console.log("Moved cursor up");
483
+ info.cursorMoved = true;
484
+ info.direction = "up";
485
+ performed = true;
486
+ }
487
+ }
488
+ break;
489
+ }
490
+ case "move_down": {
491
+ const activeElement = document.activeElement;
492
+ if (activeElement instanceof HTMLTextAreaElement) {
493
+ const lines = activeElement.value.split("\n");
494
+ const currentPos = activeElement.selectionStart || 0;
495
+ let lineStart = 0;
496
+ let currentLine = 0;
497
+ for (let i = 0; i < lines.length; i++) {
498
+ if (currentPos <= lineStart + lines[i].length) {
499
+ currentLine = i;
500
+ break;
501
+ }
502
+ lineStart += lines[i].length + 1;
503
+ }
504
+ if (currentLine < lines.length - 1) {
505
+ const posInLine = currentPos - lineStart;
506
+ const nextLineStart = lineStart + lines[currentLine].length + 1;
507
+ const newPos = Math.min(nextLineStart + posInLine, nextLineStart + lines[currentLine + 1].length);
508
+ activeElement.setSelectionRange(newPos, newPos);
509
+ console.log("Moved cursor down");
510
+ info.cursorMoved = true;
511
+ info.direction = "down";
512
+ performed = true;
513
+ }
514
+ }
515
+ break;
516
+ }
517
+ case "move_beginning": {
518
+ const activeElement = document.activeElement;
519
+ if (activeElement &&
520
+ (activeElement instanceof HTMLInputElement ||
521
+ activeElement instanceof HTMLTextAreaElement)) {
522
+ activeElement.setSelectionRange(0, 0);
523
+ console.log("Moved cursor to beginning");
524
+ info.cursorMoved = true;
525
+ info.position = "beginning";
526
+ performed = true;
527
+ }
528
+ break;
529
+ }
530
+ case "move_end": {
531
+ const activeElement = document.activeElement;
532
+ if (activeElement &&
533
+ (activeElement instanceof HTMLInputElement ||
534
+ activeElement instanceof HTMLTextAreaElement)) {
535
+ const length = activeElement.value.length;
536
+ activeElement.setSelectionRange(length, length);
537
+ console.log("Moved cursor to end");
538
+ info.cursorMoved = true;
539
+ info.position = "end";
540
+ performed = true;
541
+ }
542
+ break;
543
+ }
544
+ case "select_all": {
545
+ const activeElement = document.activeElement;
546
+ if (activeElement &&
547
+ (activeElement instanceof HTMLInputElement ||
548
+ activeElement instanceof HTMLTextAreaElement)) {
549
+ activeElement.select();
550
+ console.log("Selected all text");
551
+ info.selected = true;
552
+ performed = true;
553
+ }
554
+ else {
555
+ const selection = window.getSelection();
556
+ if (selection) {
557
+ selection.selectAllChildren(document.body);
558
+ console.log("Selected all content on page");
559
+ info.selected = true;
560
+ performed = true;
561
+ }
562
+ }
563
+ break;
564
+ }
565
+ case "select_line": {
566
+ const activeElement = document.activeElement;
567
+ if (activeElement instanceof HTMLTextAreaElement) {
568
+ const currentPos = activeElement.selectionStart || 0;
569
+ const value = activeElement.value;
570
+ let lineStart = value.lastIndexOf("\n", currentPos - 1) + 1;
571
+ let lineEnd = value.indexOf("\n", currentPos);
572
+ if (lineEnd === -1) {
573
+ lineEnd = value.length;
574
+ }
575
+ activeElement.setSelectionRange(lineStart, lineEnd);
576
+ console.log("Selected current line");
577
+ info.selected = true;
578
+ info.scope = "line";
579
+ performed = true;
580
+ }
581
+ else if (activeElement instanceof HTMLInputElement) {
582
+ activeElement.select();
583
+ console.log("Selected all text in input");
584
+ info.selected = true;
585
+ performed = true;
586
+ }
587
+ break;
588
+ }
589
+ case "type_text": {
590
+ const activeElement = document.activeElement;
591
+ const typeText = context?.parameters?.text || "";
592
+ if (activeElement &&
593
+ (activeElement instanceof HTMLInputElement ||
594
+ activeElement instanceof HTMLTextAreaElement) &&
595
+ typeText) {
596
+ const start = activeElement.selectionStart || 0;
597
+ const end = activeElement.selectionEnd || 0;
598
+ const value = activeElement.value;
599
+ activeElement.value =
600
+ value.substring(0, start) + typeText + value.substring(end);
601
+ const newPos = start + typeText.length;
602
+ activeElement.setSelectionRange(newPos, newPos);
603
+ activeElement.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
604
+ console.log(`Typed text: "${typeText}"`);
605
+ info.typed = true;
606
+ info.text = typeText;
607
+ performed = true;
608
+ }
609
+ else {
610
+ console.log("No text field is focused or no text provided");
611
+ }
612
+ break;
613
+ }
614
+ case "cut": {
615
+ const activeElement = document.activeElement;
616
+ if (activeElement &&
617
+ (activeElement instanceof HTMLInputElement ||
618
+ activeElement instanceof HTMLTextAreaElement)) {
619
+ const start = activeElement.selectionStart || 0;
620
+ const end = activeElement.selectionEnd || 0;
621
+ const selectedText = activeElement.value.substring(start, end);
622
+ if (selectedText && navigator.clipboard) {
623
+ navigator.clipboard
624
+ .writeText(selectedText)
625
+ .then(() => {
626
+ const value = activeElement.value;
627
+ activeElement.value =
628
+ value.substring(0, start) + value.substring(end);
629
+ activeElement.setSelectionRange(start, start);
630
+ activeElement.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
631
+ console.log("Cut text to clipboard");
632
+ info.cut = true;
633
+ performed = true;
634
+ })
635
+ .catch((error) => {
636
+ console.warn("Failed to cut text", error);
637
+ });
638
+ }
639
+ else {
640
+ console.log("No text selected or clipboard API unavailable");
641
+ }
642
+ }
643
+ break;
644
+ }
645
+ case "copy": {
646
+ const activeElement = document.activeElement;
647
+ if (activeElement &&
648
+ (activeElement instanceof HTMLInputElement ||
649
+ activeElement instanceof HTMLTextAreaElement)) {
650
+ const start = activeElement.selectionStart || 0;
651
+ const end = activeElement.selectionEnd || 0;
652
+ const selectedText = activeElement.value.substring(start, end);
653
+ if (selectedText && navigator.clipboard) {
654
+ navigator.clipboard
655
+ .writeText(selectedText)
656
+ .then(() => {
657
+ console.log("Copied text to clipboard");
658
+ info.copied = true;
659
+ performed = true;
660
+ })
661
+ .catch((error) => {
662
+ console.warn("Failed to copy text", error);
663
+ });
664
+ }
665
+ else {
666
+ const selection = window.getSelection();
667
+ const selectionText = selection?.toString();
668
+ if (selectionText && navigator.clipboard) {
669
+ navigator.clipboard
670
+ .writeText(selectionText)
671
+ .then(() => {
672
+ console.log("Copied selected text to clipboard");
673
+ info.copied = true;
674
+ performed = true;
675
+ })
676
+ .catch((error) => {
677
+ console.warn("Failed to copy text", error);
678
+ });
679
+ }
680
+ else {
681
+ console.log("No text selected");
682
+ }
683
+ }
684
+ }
685
+ else {
686
+ const selection = window.getSelection();
687
+ const selectionText = selection?.toString();
688
+ if (selectionText && navigator.clipboard) {
689
+ navigator.clipboard
690
+ .writeText(selectionText)
691
+ .then(() => {
692
+ console.log("Copied selected text to clipboard");
693
+ info.copied = true;
694
+ performed = true;
695
+ })
696
+ .catch((error) => {
697
+ console.warn("Failed to copy text", error);
698
+ });
699
+ }
700
+ else {
701
+ console.log("No text selected");
702
+ }
703
+ }
704
+ break;
705
+ }
706
+ case "paste": {
707
+ const activeElement = document.activeElement;
708
+ if (activeElement &&
709
+ (activeElement instanceof HTMLInputElement ||
710
+ activeElement instanceof HTMLTextAreaElement) &&
711
+ navigator.clipboard) {
712
+ navigator.clipboard
713
+ .readText()
714
+ .then((clipboardText) => {
715
+ const start = activeElement.selectionStart || 0;
716
+ const end = activeElement.selectionEnd || 0;
717
+ const value = activeElement.value;
718
+ activeElement.value =
719
+ value.substring(0, start) + clipboardText + value.substring(end);
720
+ const newPos = start + clipboardText.length;
721
+ activeElement.setSelectionRange(newPos, newPos);
722
+ activeElement.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
723
+ console.log("Pasted text from clipboard");
724
+ info.pasted = true;
725
+ performed = true;
726
+ })
727
+ .catch((error) => {
728
+ console.warn("Failed to paste text", error);
729
+ });
730
+ }
731
+ else {
732
+ console.log("No text field is focused or clipboard API unavailable");
733
+ }
734
+ break;
735
+ }
736
+ case "clear": {
737
+ const activeElement = document.activeElement;
738
+ if (activeElement &&
739
+ (activeElement instanceof HTMLInputElement ||
740
+ activeElement instanceof HTMLTextAreaElement)) {
741
+ activeElement.value = "";
742
+ activeElement.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
743
+ console.log("Cleared text field");
744
+ info.cleared = true;
745
+ performed = true;
746
+ }
747
+ break;
748
+ }
354
749
  default: {
355
750
  if (action.startsWith("navigate_") && globalPageRegistry) {
356
751
  const pageId = globalPageRegistry.getPageIdFromAction(action);
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { parseNavigationXML, fetchNavigationXML, loadNavigationPages, } from "./
8
8
  export type { PageXMLConfig } from "./services/xml-parser.js";
9
9
  export * from "./types.js";
10
10
  export * from "./actions.js";
11
- export * from "./ui.js";
11
+ export { createFloatingControl, attachToCustomButton, setState, updateStatus, updateTranscript, updateActionIndicator, showError, emitEvent, } from "./ui.js";
12
12
  import { VoiceNavigationController } from "./navigation-controller.js";
13
13
  import type { NavigationConfig } from "./types.js";
14
14
  export declare function initNavigationOnMicrophone(config: NavigationConfig): VoiceNavigationController;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,YAAY,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AAExB,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA8JnD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,gBAAgB,GACvB,yBAAyB,CAuE3B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,YAAY,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,SAAS,EACT,SAAS,GACV,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA+JnD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,gBAAgB,GACvB,yBAAyB,CAoG3B"}
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ export { PageRegistry } from "./services/page-registry.js";
6
6
  export { parseNavigationXML, fetchNavigationXML, loadNavigationPages, } from "./services/xml-parser.js";
7
7
  export * from "./types.js";
8
8
  export * from "./actions.js";
9
- export * from "./ui.js";
9
+ export { createFloatingControl, attachToCustomButton, setState, updateStatus, updateTranscript, updateActionIndicator, showError, emitEvent, } from "./ui.js";
10
10
  import { VoiceNavigationController } from "./navigation-controller.js";
11
11
  const CONFIG_STORAGE_KEY = "__voice_navigation_config";
12
12
  const resolveOpenSearchConfig = (config) => {
@@ -37,6 +37,7 @@ const buildPersistableConfig = (config) => {
37
37
  pages: normalizedConfig.pages ? { ...normalizedConfig.pages } : undefined,
38
38
  language: normalizedConfig.language,
39
39
  autoStart: normalizedConfig.autoStart,
40
+ ui: normalizedConfig.ui ? { ...normalizedConfig.ui } : undefined,
40
41
  };
41
42
  if (hasCustomHandlers) {
42
43
  persistable.hasCustomHandlers = true;
@@ -92,6 +93,7 @@ export function initNavigationOnMicrophone(config) {
92
93
  }
93
94
  const globalWindow = window;
94
95
  const normalizedConfig = normalizeNavigationConfig(config);
96
+ console.log("[Voice Navigation Init] UI Config received:", normalizedConfig.ui);
95
97
  const previousConfigRaw = globalWindow.__voiceNavigationConfig;
96
98
  const previousConfig = previousConfigRaw
97
99
  ? normalizeNavigationConfig(previousConfigRaw)
@@ -107,6 +109,7 @@ export function initNavigationOnMicrophone(config) {
107
109
  console.warn("Failed to persist voice navigation config:", error);
108
110
  }
109
111
  if (globalWindow.__navigateOnMicrophoneInitialized) {
112
+ console.log("[Voice Navigation] Already initialized, checking for config changes...");
110
113
  const existingController = globalWindow.navigationController;
111
114
  const previousOpenSearchConfig = resolveOpenSearchConfig(previousConfig);
112
115
  const currentOpenSearchConfig = resolveOpenSearchConfig(normalizedConfig);
@@ -118,8 +121,20 @@ export function initNavigationOnMicrophone(config) {
118
121
  wantsVectorSearch &&
119
122
  describeOpenSearchConfig(previousOpenSearchConfig) !==
120
123
  describeOpenSearchConfig(currentOpenSearchConfig);
121
- if (wantsVectorSearch &&
122
- (!hadVectorSearch || vectorConfigChanged || !existingHasVectorSearch)) {
124
+ const previousUI = previousConfig?.ui;
125
+ const currentUI = normalizedConfig.ui;
126
+ console.log("[Voice Navigation] Previous UI config:", previousUI);
127
+ console.log("[Voice Navigation] Current UI config:", currentUI);
128
+ const uiConfigChanged = previousUI?.showDefaultButton !== currentUI?.showDefaultButton ||
129
+ previousUI?.customButtonSelector !== currentUI?.customButtonSelector ||
130
+ previousUI?.buttonPlacement !== currentUI?.buttonPlacement;
131
+ console.log("[Voice Navigation] UI config changed:", uiConfigChanged);
132
+ if ((wantsVectorSearch &&
133
+ (!hadVectorSearch ||
134
+ vectorConfigChanged ||
135
+ !existingHasVectorSearch)) ||
136
+ uiConfigChanged) {
137
+ console.log("[Voice Navigation] Destroying existing controller and recreating...");
123
138
  try {
124
139
  existingController?.destroy?.();
125
140
  }
@@ -14,7 +14,9 @@ export declare class VoiceNavigationController {
14
14
  private removeAutoStartListeners;
15
15
  private vectorSearchService;
16
16
  private pagesInitialized;
17
+ private uiInitialized;
17
18
  constructor(config: NavigationConfig);
19
+ private initializeUIAsync;
18
20
  private shouldAutoStart;
19
21
  setAutoStart(enabled: boolean): void;
20
22
  prepareForNavigation(options?: {
@@ -1 +1 @@
1
- {"version":3,"file":"navigation-controller.d.ts","sourceRoot":"","sources":["../src/navigation-controller.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAKjB,MAAM,YAAY,CAAC;AA0DpB,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,EAAE,CAAa;IACvB,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,qBAAqB,CAAkB;IAC/C,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,+BAA+B,CAAkB;IACzD,OAAO,CAAC,wBAAwB,CAA6B;IAC7D,OAAO,CAAC,mBAAmB,CAAoC;IAC/D,OAAO,CAAC,gBAAgB,CAAgB;gBAE5B,MAAM,EAAE,gBAAgB;IAgFpC,OAAO,CAAC,eAAe;IAoBhB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAepC,oBAAoB,CAAC,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,IAAI;YAOtD,eAAe;IAmE7B,OAAO,CAAC,kBAAkB;YASZ,eAAe;IAkBhB,KAAK,CAChB,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAO,GACzC,OAAO,CAAC,IAAI,CAAC;IA0EH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2BlC,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,aAAa;YAWP,aAAa;YAOb,wBAAwB;IAuCtC,OAAO,CAAC,WAAW;YAoBL,iBAAiB;YAwBjB,iBAAiB;IA0HxB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;YAI5B,yBAAyB;IAgMvC,OAAO,CAAC,oBAAoB;IAqD5B,OAAO,CAAC,yBAAyB;IAoCjC,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,4BAA4B;IAiB7B,OAAO,IAAI,IAAI;IAgBtB,OAAO,CAAC,kBAAkB;IA8B1B,OAAO,CAAC,kBAAkB;IAwD1B,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,uBAAuB;IAkD/B,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,wBAAwB;CAmBjC"}
1
+ {"version":3,"file":"navigation-controller.d.ts","sourceRoot":"","sources":["../src/navigation-controller.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAKjB,MAAM,YAAY,CAAC;AA2DpB,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,qBAAqB,CAAkB;IAC/C,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,+BAA+B,CAAkB;IACzD,OAAO,CAAC,wBAAwB,CAA6B;IAC7D,OAAO,CAAC,mBAAmB,CAAoC;IAC/D,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,aAAa,CAAgB;gBAEzB,MAAM,EAAE,gBAAgB;YAkFtB,iBAAiB;IAiD/B,OAAO,CAAC,eAAe;IAoBhB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAepC,oBAAoB,CAAC,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,IAAI;YAOtD,eAAe;IAmE7B,OAAO,CAAC,kBAAkB;YASZ,eAAe;IAwBhB,KAAK,CAChB,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAO,GACzC,OAAO,CAAC,IAAI,CAAC;IAmFH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiClC,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,aAAa;YAWP,aAAa;YAOb,wBAAwB;IAuCtC,OAAO,CAAC,WAAW;YAoBL,iBAAiB;YAwBjB,iBAAiB;IA0HxB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;YAI5B,yBAAyB;IAgMvC,OAAO,CAAC,oBAAoB;IAqD5B,OAAO,CAAC,yBAAyB;IAoCjC,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,4BAA4B;IAiB7B,OAAO,IAAI,IAAI;IAgBtB,OAAO,CAAC,kBAAkB;IA8B1B,OAAO,CAAC,kBAAkB;IAwD1B,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,uBAAuB;IAkD/B,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,wBAAwB;CAmBjC"}
@@ -5,7 +5,7 @@ import { PageRegistry } from "./services/page-registry.js";
5
5
  import { loadNavigationPages } from "./services/xml-parser.js";
6
6
  import { VectorSearchService, } from "./services/vector-search.js";
7
7
  import { performAgentAction, formatActionLabel, extractAgentAction, setPageRegistry, } from "./actions.js";
8
- import { createFloatingControl, setState, updateStatus, updateTranscript, updateActionIndicator, showError, emitEvent, } from "./ui.js";
8
+ import { createFloatingControl, attachToCustomButton, setState, updateStatus, updateTranscript, updateActionIndicator, showError, emitEvent, } from "./ui.js";
9
9
  const AUTO_START_STORAGE_KEY = "__navigate_auto_start";
10
10
  const RESUME_STATE_STORAGE_KEY = "__navigate_resume_state";
11
11
  const flowLog = (...args) => {
@@ -42,6 +42,7 @@ export class VoiceNavigationController {
42
42
  ...config,
43
43
  opensearch: openSearchConfig,
44
44
  };
45
+ console.log("[Voice Navigation Controller] Constructor called with UI config:", this.config.ui);
45
46
  if (!this.config.azure?.subscriptionKey || !this.config.azure?.region) {
46
47
  throw new Error("Azure Speech configuration is required");
47
48
  }
@@ -80,8 +81,7 @@ export class VoiceNavigationController {
80
81
  }
81
82
  }
82
83
  this.resumeAfterNavigation = this.consumeResumeState();
83
- this.ui = createFloatingControl();
84
- this.setupEventHandlers();
84
+ this.uiInitialized = this.initializeUIAsync();
85
85
  if (this.resumeAfterNavigation) {
86
86
  void this.start({ reason: "auto" });
87
87
  }
@@ -89,6 +89,39 @@ export class VoiceNavigationController {
89
89
  void this.start({ reason: "auto" });
90
90
  }
91
91
  }
92
+ async initializeUIAsync() {
93
+ const uiConfig = this.config.ui || {};
94
+ const showDefaultButton = uiConfig.showDefaultButton !== false;
95
+ const customButtonSelector = uiConfig.customButtonSelector;
96
+ const buttonPlacement = uiConfig.buttonPlacement || "bottom-right";
97
+ console.log("[Voice Navigation] UI Config:", {
98
+ showDefaultButton,
99
+ customButtonSelector,
100
+ buttonPlacement,
101
+ });
102
+ if (!showDefaultButton && customButtonSelector) {
103
+ console.log(`[Voice Navigation] Attempting to use custom button: ${customButtonSelector}`);
104
+ try {
105
+ this.ui = await attachToCustomButton(customButtonSelector);
106
+ flowLog(`✓ Attached to custom button: ${customButtonSelector}`);
107
+ }
108
+ catch (error) {
109
+ console.error("Failed to attach to custom button:", error);
110
+ console.warn("Falling back to default button due to custom button error");
111
+ this.ui = createFloatingControl(buttonPlacement);
112
+ }
113
+ }
114
+ else if (!showDefaultButton && !customButtonSelector) {
115
+ console.warn("showDefaultButton is false but no customButtonSelector provided. Creating default button.");
116
+ this.ui = createFloatingControl(buttonPlacement);
117
+ }
118
+ else {
119
+ console.log(`[Voice Navigation] Creating default button at: ${buttonPlacement}`);
120
+ this.ui = createFloatingControl(buttonPlacement);
121
+ flowLog(`✓ Created default button at position: ${buttonPlacement}`);
122
+ }
123
+ this.setupEventHandlers();
124
+ }
92
125
  shouldAutoStart() {
93
126
  if (this.config.autoStart === true) {
94
127
  return true;
@@ -164,6 +197,10 @@ export class VoiceNavigationController {
164
197
  });
165
198
  }
166
199
  async toggleRecording() {
200
+ if (!this.ui || !this.ui.root) {
201
+ console.warn("UI not initialized, cannot toggle recording");
202
+ return;
203
+ }
167
204
  const currentState = this.ui.root.dataset.state;
168
205
  if (currentState === "listening" || currentState === "thinking") {
169
206
  await this.stop();
@@ -177,6 +214,11 @@ export class VoiceNavigationController {
177
214
  async start(options = {}) {
178
215
  const reason = options.reason ?? "user";
179
216
  this.lastStartReason = reason;
217
+ await this.uiInitialized;
218
+ if (!this.ui || !this.ui.root) {
219
+ console.error("UI not properly initialized");
220
+ return;
221
+ }
180
222
  try {
181
223
  await this.pagesInitialized;
182
224
  }
@@ -239,6 +281,10 @@ export class VoiceNavigationController {
239
281
  }
240
282
  }
241
283
  async stop() {
284
+ if (!this.ui || !this.ui.root) {
285
+ console.warn("UI not initialized, cannot stop");
286
+ return;
287
+ }
242
288
  this.isProcessing = false;
243
289
  setState(this.ui, "thinking");
244
290
  emitEvent("state-change", { state: "thinking" });
@@ -693,7 +739,7 @@ export class VoiceNavigationController {
693
739
  }
694
740
  destroy() {
695
741
  this.clearAutoStartFallback();
696
- if (this.ui.root.parentElement) {
742
+ if (this.ui && this.ui.root && this.ui.root.parentElement) {
697
743
  this.ui.root.parentElement.removeChild(this.ui.root);
698
744
  }
699
745
  const styles = document.getElementById("navigate-control-styles");
package/dist/types.d.ts CHANGED
@@ -1,5 +1,11 @@
1
- export type CoreNavigationAction = "zoom_in" | "zoom_out" | "scroll_up" | "scroll_down" | "scroll_left" | "scroll_right" | "page_up" | "page_down" | "scroll_top" | "scroll_bottom" | "go_back" | "go_forward" | "reload_page" | "print_page" | "copy_url" | "open_menu" | "close_menu" | "focus_search" | "toggle_fullscreen" | "exit_fullscreen" | "play_media" | "pause_media" | "mute_media" | "unmute_media" | "search_content" | "stop" | "unknown";
1
+ export type CoreNavigationAction = "zoom_in" | "zoom_out" | "scroll_up" | "scroll_down" | "scroll_left" | "scroll_right" | "page_up" | "page_down" | "scroll_top" | "scroll_bottom" | "go_back" | "go_forward" | "reload_page" | "print_page" | "copy_url" | "open_menu" | "close_menu" | "focus_search" | "toggle_fullscreen" | "exit_fullscreen" | "play_media" | "pause_media" | "mute_media" | "unmute_media" | "search_content" | "stop" | "quit" | "tab_next" | "tab_back" | "click" | "enter" | "submit" | "move_left" | "move_right" | "move_up" | "move_down" | "move_beginning" | "move_end" | "select_all" | "select_line" | "type_text" | "cut" | "copy" | "paste" | "clear" | "unknown";
2
2
  export type NavigationAction = CoreNavigationAction | `navigate_${string}`;
3
+ export type ButtonPlacement = "bottom-right" | "bottom-left" | "top-right" | "top-left" | "center-right" | "center-left";
4
+ export interface UIConfig {
5
+ showDefaultButton?: boolean;
6
+ customButtonSelector?: string;
7
+ buttonPlacement?: ButtonPlacement;
8
+ }
3
9
  export interface NavigationConfig {
4
10
  aws?: {
5
11
  region?: string;
@@ -18,6 +24,7 @@ export interface NavigationConfig {
18
24
  opensearch?: OpenSearchConfig;
19
25
  openSearch?: OpenSearchConfig;
20
26
  pages?: NavigationPagesConfig;
27
+ ui?: UIConfig;
21
28
  }
22
29
  export interface NavigationPagesConfig {
23
30
  xml?: string;
@@ -102,11 +109,12 @@ export interface NavigationCallbacks {
102
109
  export type NavigationState = "idle" | "loading" | "listening" | "thinking" | "error";
103
110
  export interface UIElements {
104
111
  root: HTMLElement;
105
- button: HTMLButtonElement;
106
- label: HTMLElement;
112
+ button: HTMLButtonElement | HTMLElement;
113
+ label: HTMLElement | null;
107
114
  status: HTMLElement;
108
115
  transcript: HTMLElement;
109
116
  action: HTMLElement;
117
+ isCustomButton?: boolean;
110
118
  }
111
119
  export interface NavigationEvent extends CustomEvent {
112
120
  detail: {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/ui.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { UIElements, NavigationState } from "./types.js";
2
- export declare const injectFloatingControlStyles: () => void;
3
- export declare const createFloatingControl: () => UIElements;
1
+ import type { UIElements, NavigationState, ButtonPlacement } from "./types.js";
2
+ export declare const injectFloatingControlStyles: (placement?: ButtonPlacement) => void;
3
+ export declare const createFloatingControl: (placement?: ButtonPlacement) => UIElements;
4
+ export declare const attachToCustomButton: (buttonSelector: string) => Promise<UIElements>;
4
5
  export declare const updateStatus: (ui: UIElements, text: string) => void;
5
6
  export declare const setState: (ui: UIElements, state: NavigationState) => void;
6
7
  export declare const updateTranscript: (ui: UIElements, text: string) => void;
package/dist/ui.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAK9D,eAAO,MAAM,2BAA2B,QAAO,IAmH9C,CAAC;AAKF,eAAO,MAAM,qBAAqB,QAAO,UAqDxC,CAAC;AAKF,eAAO,MAAM,YAAY,GAAI,IAAI,UAAU,EAAE,MAAM,MAAM,KAAG,IAE3D,CAAC;AAKF,eAAO,MAAM,QAAQ,GAAI,IAAI,UAAU,EAAE,OAAO,eAAe,KAAG,IA+BjE,CAAC;AAKF,eAAO,MAAM,gBAAgB,GAAI,IAAI,UAAU,EAAE,MAAM,MAAM,KAAG,IAE/D,CAAC;AAKF,eAAO,MAAM,qBAAqB,GAChC,IAAI,UAAU,EACd,YAAY,MAAM,EAClB,WAAW,OAAO,EAClB,OAAM,GAAQ,KACb,IAkBF,CAAC;AAKF,eAAO,MAAM,SAAS,GAAI,IAAI,UAAU,EAAE,OAAO,KAAK,KAAG,IAGxD,CAAC;AAKF,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,EAAE,SAAQ,GAAQ,KAAG,IAU1D,CAAC"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAuC/E,eAAO,MAAM,2BAA2B,GACtC,YAAW,eAAgC,KAC1C,IAqHF,CAAC;AAKF,eAAO,MAAM,qBAAqB,GAChC,YAAW,eAAgC,KAC1C,UAgEF,CAAC;AA+EF,eAAO,MAAM,oBAAoB,GAC/B,gBAAgB,MAAM,KACrB,OAAO,CAAC,UAAU,CAuEpB,CAAC;AAKF,eAAO,MAAM,YAAY,GAAI,IAAI,UAAU,EAAE,MAAM,MAAM,KAAG,IAE3D,CAAC;AAKF,eAAO,MAAM,QAAQ,GAAI,IAAI,UAAU,EAAE,OAAO,eAAe,KAAG,IAgDjE,CAAC;AAKF,eAAO,MAAM,gBAAgB,GAAI,IAAI,UAAU,EAAE,MAAM,MAAM,KAAG,IAE/D,CAAC;AAKF,eAAO,MAAM,qBAAqB,GAChC,IAAI,UAAU,EACd,YAAY,MAAM,EAClB,WAAW,OAAO,EAClB,OAAM,GAAQ,KACb,IAkBF,CAAC;AAKF,eAAO,MAAM,SAAS,GAAI,IAAI,UAAU,EAAE,OAAO,KAAK,KAAG,IAGxD,CAAC;AAKF,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,EAAE,SAAQ,GAAQ,KAAG,IAU1D,CAAC"}
package/dist/ui.js CHANGED
@@ -1,17 +1,41 @@
1
- export const injectFloatingControlStyles = () => {
2
- if (document.getElementById("navigate-control-styles")) {
3
- return;
1
+ const getPlacementStyles = (placement) => {
2
+ const positions = {
3
+ "bottom-right": "bottom: 24px; right: 24px; left: auto; top: auto;",
4
+ "bottom-left": "bottom: 24px; left: 24px; right: auto; top: auto;",
5
+ "top-right": "top: 24px; right: 24px; left: auto; bottom: auto;",
6
+ "top-left": "top: 24px; left: 24px; right: auto; bottom: auto;",
7
+ "center-right": "top: 50%; right: 24px; left: auto; bottom: auto; transform: translateY(-50%);",
8
+ "center-left": "top: 50%; left: 24px; right: auto; bottom: auto; transform: translateY(-50%);",
9
+ };
10
+ return positions[placement] || positions["bottom-right"];
11
+ };
12
+ const getMobilePlacementStyles = (placement) => {
13
+ const positions = {
14
+ "bottom-right": "bottom: 16px; right: 16px; left: auto; top: auto;",
15
+ "bottom-left": "bottom: 16px; left: 16px; right: auto; top: auto;",
16
+ "top-right": "top: 16px; right: 16px; left: auto; bottom: auto;",
17
+ "top-left": "top: 16px; left: 16px; right: auto; bottom: auto;",
18
+ "center-right": "top: 50%; right: 16px; left: auto; bottom: auto; transform: translateY(-50%);",
19
+ "center-left": "top: 50%; left: 16px; right: auto; bottom: auto; transform: translateY(-50%);",
20
+ };
21
+ return positions[placement] || positions["bottom-right"];
22
+ };
23
+ export const injectFloatingControlStyles = (placement = "bottom-right") => {
24
+ const existingStyle = document.getElementById("navigate-control-styles");
25
+ if (existingStyle) {
26
+ existingStyle.remove();
4
27
  }
5
28
  const style = document.createElement("style");
6
29
  style.id = "navigate-control-styles";
30
+ const placementStyles = getPlacementStyles(placement);
31
+ const mobilePlacementStyles = getMobilePlacementStyles(placement);
7
32
  style.textContent = `
8
33
  .navigate-control {
9
34
  position: fixed;
10
- bottom: 24px;
11
- right: 24px;
35
+ ${placementStyles}
12
36
  display: flex;
13
37
  gap: 12px;
14
- align-items: flex-end;
38
+ align-items: ${placement.startsWith("top") ? "flex-start" : "flex-end"};
15
39
  z-index: 2147483647;
16
40
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
17
41
  color: #0f172a;
@@ -101,8 +125,7 @@ export const injectFloatingControlStyles = () => {
101
125
 
102
126
  @media (max-width: 640px) {
103
127
  .navigate-control {
104
- right: 16px;
105
- bottom: 16px;
128
+ ${mobilePlacementStyles}
106
129
  }
107
130
 
108
131
  .navigate-control__panel {
@@ -112,8 +135,8 @@ export const injectFloatingControlStyles = () => {
112
135
  `;
113
136
  document.head.appendChild(style);
114
137
  };
115
- export const createFloatingControl = () => {
116
- injectFloatingControlStyles();
138
+ export const createFloatingControl = (placement = "bottom-right") => {
139
+ injectFloatingControlStyles(placement);
117
140
  let root = document.getElementById("navigate-control");
118
141
  if (!root) {
119
142
  root = document.createElement("div");
@@ -133,10 +156,12 @@ export const createFloatingControl = () => {
133
156
  `;
134
157
  const appendToBody = () => {
135
158
  if (!document.body) {
159
+ console.warn("Voice navigation: document.body not available yet");
136
160
  return;
137
161
  }
138
162
  if (!document.body.contains(root)) {
139
163
  document.body.appendChild(root);
164
+ console.log(`Voice navigation: Button created with placement "${placement}"`);
140
165
  }
141
166
  };
142
167
  appendToBody();
@@ -145,13 +170,111 @@ export const createFloatingControl = () => {
145
170
  once: true,
146
171
  });
147
172
  }
173
+ const button = root.querySelector(".navigate-control__button");
174
+ if (!button) {
175
+ console.error("Voice navigation: Failed to create button element");
176
+ }
148
177
  return {
149
178
  root,
150
- button: root.querySelector(".navigate-control__button"),
179
+ button,
151
180
  label: root.querySelector(".navigate-control__label"),
152
181
  status: root.querySelector(".navigate-control__status"),
153
182
  transcript: root.querySelector(".navigate-control__transcript"),
154
183
  action: root.querySelector(".navigate-control__action"),
184
+ isCustomButton: false,
185
+ };
186
+ };
187
+ const waitForElement = (selector, timeout = 30000) => {
188
+ return new Promise((resolve, reject) => {
189
+ const element = document.querySelector(selector);
190
+ if (element) {
191
+ console.log(`[Voice Navigation] Element "${selector}" found immediately`);
192
+ resolve(element);
193
+ return;
194
+ }
195
+ console.log(`[Voice Navigation] Element "${selector}" not found, waiting up to ${timeout}ms...`);
196
+ let observer = null;
197
+ const waitForBody = () => {
198
+ if (!document.body) {
199
+ console.log("[Voice Navigation] Waiting for document.body to be available...");
200
+ setTimeout(waitForBody, 100);
201
+ return;
202
+ }
203
+ observer = new MutationObserver(() => {
204
+ const element = document.querySelector(selector);
205
+ if (element) {
206
+ observer?.disconnect();
207
+ clearTimeout(timeoutId);
208
+ console.log(`[Voice Navigation] Element "${selector}" found after waiting`);
209
+ resolve(element);
210
+ }
211
+ });
212
+ observer.observe(document.body, {
213
+ childList: true,
214
+ subtree: true,
215
+ });
216
+ };
217
+ waitForBody();
218
+ const timeoutId = setTimeout(() => {
219
+ observer?.disconnect();
220
+ console.error(`[Voice Navigation] Timeout: Element "${selector}" not found after ${timeout}ms`);
221
+ const allButtons = document.querySelectorAll('[id^="btn"]');
222
+ if (allButtons.length > 0) {
223
+ console.log("[Voice Navigation] Available buttons with id starting with 'btn':", Array.from(allButtons).map((btn) => `#${btn.id}`));
224
+ }
225
+ reject(new Error(`Custom button not found with selector: "${selector}" after ${timeout}ms. Make sure the button exists in the DOM.`));
226
+ }, timeout);
227
+ });
228
+ };
229
+ export const attachToCustomButton = async (buttonSelector) => {
230
+ injectFloatingControlStyles("bottom-right");
231
+ console.log(`Voice navigation: Waiting for custom button "${buttonSelector}"...`);
232
+ const customButton = await waitForElement(buttonSelector).catch((error) => {
233
+ console.error(error.message);
234
+ throw error;
235
+ });
236
+ console.log(`Voice navigation: Custom button found "${buttonSelector}"`);
237
+ let root = document.getElementById("navigate-control-panel");
238
+ if (!root) {
239
+ root = document.createElement("div");
240
+ root.id = "navigate-control-panel";
241
+ root.className = "navigate-control";
242
+ root.style.display = "none";
243
+ }
244
+ root.dataset.state = root.dataset.state || "idle";
245
+ root.innerHTML = `
246
+ <div class="navigate-control__panel">
247
+ <div class="navigate-control__status">Tap your button to start voice control</div>
248
+ <div class="navigate-control__transcript" role="status" aria-live="polite"></div>
249
+ <div class="navigate-control__action" aria-live="polite"></div>
250
+ </div>
251
+ `;
252
+ const appendToBody = () => {
253
+ if (!document.body) {
254
+ return;
255
+ }
256
+ if (!document.body.contains(root)) {
257
+ document.body.appendChild(root);
258
+ }
259
+ };
260
+ appendToBody();
261
+ if (document.readyState === "loading") {
262
+ document.addEventListener("DOMContentLoaded", appendToBody, {
263
+ once: true,
264
+ });
265
+ }
266
+ customButton.setAttribute("aria-pressed", "false");
267
+ if (!customButton.getAttribute("aria-label")) {
268
+ customButton.setAttribute("aria-label", "Start voice control");
269
+ }
270
+ return {
271
+ root,
272
+ button: customButton,
273
+ label: null,
274
+ status: root.querySelector(".navigate-control__status"),
275
+ transcript: root.querySelector(".navigate-control__transcript"),
276
+ action: root.querySelector(".navigate-control__action"),
277
+ isCustomButton: true,
155
278
  };
156
279
  };
157
280
  export const updateStatus = (ui, text) => {
@@ -159,34 +282,44 @@ export const updateStatus = (ui, text) => {
159
282
  };
160
283
  export const setState = (ui, state) => {
161
284
  ui.root.dataset.state = state;
285
+ const updateLabel = (text) => {
286
+ if (ui.label) {
287
+ ui.label.textContent = text;
288
+ }
289
+ };
162
290
  switch (state) {
163
291
  case "idle":
164
- ui.label.textContent = "Start";
292
+ updateLabel("Start");
165
293
  ui.button.setAttribute("aria-pressed", "false");
166
294
  ui.button.setAttribute("aria-label", "Start voice control");
167
- updateStatus(ui, "Tap to start voice control");
295
+ updateStatus(ui, ui.isCustomButton
296
+ ? "Tap your button to start voice control"
297
+ : "Tap to start voice control");
168
298
  break;
169
299
  case "loading":
170
- ui.label.textContent = "Loading...";
300
+ updateLabel("Loading...");
171
301
  ui.button.setAttribute("aria-label", "Loading voice control");
172
302
  updateStatus(ui, "Loading...");
173
303
  break;
174
304
  case "listening":
175
- ui.label.textContent = "Stop";
305
+ updateLabel("Stop");
176
306
  ui.button.setAttribute("aria-pressed", "true");
177
307
  ui.button.setAttribute("aria-label", "Stop voice control");
178
308
  updateStatus(ui, "Listening...");
179
309
  break;
180
310
  case "thinking":
181
- ui.label.textContent = "Stop";
311
+ updateLabel("Stop");
182
312
  ui.button.setAttribute("aria-label", "Processing");
183
313
  updateStatus(ui, "Processing...");
184
314
  break;
185
315
  case "error":
186
- ui.label.textContent = "Error";
316
+ updateLabel("Error");
187
317
  ui.button.setAttribute("aria-label", "Error occurred");
188
318
  break;
189
319
  }
320
+ if (ui.isCustomButton) {
321
+ ui.root.style.display = state === "idle" ? "none" : "flex";
322
+ }
190
323
  };
191
324
  export const updateTranscript = (ui, text) => {
192
325
  ui.transcript.textContent = text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myscheme/voice-navigation-sdk",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Voice navigation SDK using Azure Speech and AWS Bedrock",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",