@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 +250 -1
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +395 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -3
- package/dist/navigation-controller.d.ts +2 -0
- package/dist/navigation-controller.d.ts.map +1 -1
- package/dist/navigation-controller.js +50 -4
- package/dist/types.d.ts +11 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +4 -3
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +150 -17
- package/package.json +1 -1
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
|
-
-
|
|
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.
|
package/dist/actions.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
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;
|
|
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.
|
|
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: {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,oBAAoB,GAC5B,SAAS,GACT,UAAU,GACV,WAAW,GACX,aAAa,GACb,aAAa,GACb,cAAc,GACd,SAAS,GACT,WAAW,GACX,YAAY,GACZ,eAAe,GACf,SAAS,GACT,YAAY,GACZ,aAAa,GACb,YAAY,GACZ,UAAU,GACV,WAAW,GACX,YAAY,GACZ,cAAc,GACd,mBAAmB,GACnB,iBAAiB,GACjB,YAAY,GACZ,aAAa,GACb,YAAY,GACZ,cAAc,GACd,gBAAgB,GAChB,MAAM,GACN,SAAS,CAAC;AAKd,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GAAG,YAAY,MAAM,EAAE,CAAC;
|
|
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;
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
292
|
+
updateLabel("Start");
|
|
165
293
|
ui.button.setAttribute("aria-pressed", "false");
|
|
166
294
|
ui.button.setAttribute("aria-label", "Start voice control");
|
|
167
|
-
updateStatus(ui,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
+
updateLabel("Stop");
|
|
182
312
|
ui.button.setAttribute("aria-label", "Processing");
|
|
183
313
|
updateStatus(ui, "Processing...");
|
|
184
314
|
break;
|
|
185
315
|
case "error":
|
|
186
|
-
|
|
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;
|