@myscheme/voice-navigation-sdk 0.1.0

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.
Files changed (41) hide show
  1. package/README.md +359 -0
  2. package/dist/actions.d.ts +8 -0
  3. package/dist/actions.d.ts.map +1 -0
  4. package/dist/actions.js +478 -0
  5. package/dist/constants.d.ts +2 -0
  6. package/dist/constants.d.ts.map +1 -0
  7. package/dist/constants.js +1 -0
  8. package/dist/index.d.ts +11 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +156 -0
  11. package/dist/microphone-handler.d.ts +47 -0
  12. package/dist/microphone-handler.d.ts.map +1 -0
  13. package/dist/microphone-handler.js +341 -0
  14. package/dist/navigation-controller.d.ts +50 -0
  15. package/dist/navigation-controller.d.ts.map +1 -0
  16. package/dist/navigation-controller.js +782 -0
  17. package/dist/server/index.d.ts +3 -0
  18. package/dist/server/index.d.ts.map +1 -0
  19. package/dist/server/index.js +1 -0
  20. package/dist/server/opensearch-handler.d.ts +52 -0
  21. package/dist/server/opensearch-handler.d.ts.map +1 -0
  22. package/dist/server/opensearch-handler.js +279 -0
  23. package/dist/services/azure-speech.d.ts +13 -0
  24. package/dist/services/azure-speech.d.ts.map +1 -0
  25. package/dist/services/azure-speech.js +33 -0
  26. package/dist/services/bedrock.d.ts +18 -0
  27. package/dist/services/bedrock.d.ts.map +1 -0
  28. package/dist/services/bedrock.js +132 -0
  29. package/dist/services/schemes.d.ts +2 -0
  30. package/dist/services/schemes.d.ts.map +1 -0
  31. package/dist/services/schemes.js +1 -0
  32. package/dist/services/vector-search.d.ts +21 -0
  33. package/dist/services/vector-search.d.ts.map +1 -0
  34. package/dist/services/vector-search.js +181 -0
  35. package/dist/types.d.ts +107 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +1 -0
  38. package/dist/ui.d.ts +10 -0
  39. package/dist/ui.d.ts.map +1 -0
  40. package/dist/ui.js +225 -0
  41. package/package.json +55 -0
@@ -0,0 +1,478 @@
1
+ const ALLOWED_ACTIONS = new Set([
2
+ "zoom_in",
3
+ "zoom_out",
4
+ "scroll_up",
5
+ "scroll_down",
6
+ "scroll_left",
7
+ "scroll_right",
8
+ "page_up",
9
+ "page_down",
10
+ "scroll_top",
11
+ "scroll_bottom",
12
+ "go_back",
13
+ "go_forward",
14
+ "reload_page",
15
+ "print_page",
16
+ "copy_url",
17
+ "open_menu",
18
+ "close_menu",
19
+ "focus_search",
20
+ "toggle_fullscreen",
21
+ "exit_fullscreen",
22
+ "play_media",
23
+ "pause_media",
24
+ "mute_media",
25
+ "unmute_media",
26
+ "navigate_home",
27
+ "search_content",
28
+ "navigate_search",
29
+ "navigate_faqs",
30
+ "navigate_help",
31
+ "navigate_contact",
32
+ "navigate_about",
33
+ "navigate_screen_reader",
34
+ "navigate_accessibility",
35
+ "navigate_disclaimer",
36
+ "navigate_terms_conditions",
37
+ "stop",
38
+ "unknown",
39
+ ]);
40
+ const ZOOM_STEP = 0.1;
41
+ const MIN_ZOOM = 0.5;
42
+ const MAX_ZOOM = 2.0;
43
+ const SCROLL_AMOUNT = 240;
44
+ const notifyNavigationIntent = (context, target) => {
45
+ const handler = context?.handler;
46
+ if (handler?.prepareForNavigation) {
47
+ handler.prepareForNavigation({ target });
48
+ }
49
+ };
50
+ const ensureZoomBaseline = () => {
51
+ const root = document.documentElement;
52
+ if (!root.dataset.navigateZoom) {
53
+ root.dataset.navigateZoom = "1";
54
+ root.style.setProperty("--navigate-zoom-scale", "1");
55
+ }
56
+ };
57
+ export const setZoomScale = (scale) => {
58
+ ensureZoomBaseline();
59
+ const clamped = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, scale));
60
+ const rounded = parseFloat(clamped.toFixed(2));
61
+ const root = document.documentElement;
62
+ root.dataset.navigateZoom = String(rounded);
63
+ root.style.setProperty("--navigate-zoom-scale", String(rounded));
64
+ root.style.fontSize = `${clamped * 100}%`;
65
+ return clamped;
66
+ };
67
+ export const adjustZoom = (delta) => {
68
+ ensureZoomBaseline();
69
+ const current = parseFloat(document.documentElement.dataset.navigateZoom || "1");
70
+ return setZoomScale(current + delta);
71
+ };
72
+ export const formatActionLabel = (action) => {
73
+ return action.replace(/_/g, " ");
74
+ };
75
+ export const performAgentAction = (action, context = {}) => {
76
+ if (!action) {
77
+ return { performed: false, info: {} };
78
+ }
79
+ document.documentElement.dataset.navigateRequestedAction = action;
80
+ let performed = false;
81
+ const info = {};
82
+ const onStop = context?.onStop;
83
+ switch (action) {
84
+ case "zoom_in": {
85
+ const newZoom = adjustZoom(ZOOM_STEP);
86
+ console.log(`Zoomed in to ${newZoom}`);
87
+ info.newZoom = newZoom;
88
+ performed = true;
89
+ break;
90
+ }
91
+ case "zoom_out": {
92
+ const newZoom = adjustZoom(-ZOOM_STEP);
93
+ console.log(`Zoomed out to ${newZoom}`);
94
+ info.newZoom = newZoom;
95
+ performed = true;
96
+ break;
97
+ }
98
+ case "scroll_up": {
99
+ window.scrollBy({ top: -SCROLL_AMOUNT, behavior: "smooth" });
100
+ console.log("Scrolled up");
101
+ info.scrolled = true;
102
+ info.direction = "up";
103
+ performed = true;
104
+ break;
105
+ }
106
+ case "scroll_down": {
107
+ window.scrollBy({ top: SCROLL_AMOUNT, behavior: "smooth" });
108
+ console.log("Scrolled down");
109
+ info.scrolled = true;
110
+ info.direction = "down";
111
+ performed = true;
112
+ break;
113
+ }
114
+ case "scroll_left": {
115
+ window.scrollBy({ left: -SCROLL_AMOUNT, behavior: "smooth" });
116
+ console.log("Scrolled left");
117
+ info.scrolled = true;
118
+ info.direction = "left";
119
+ performed = true;
120
+ break;
121
+ }
122
+ case "scroll_right": {
123
+ window.scrollBy({ left: SCROLL_AMOUNT, behavior: "smooth" });
124
+ console.log("Scrolled right");
125
+ info.scrolled = true;
126
+ info.direction = "right";
127
+ performed = true;
128
+ break;
129
+ }
130
+ case "page_up": {
131
+ window.scrollBy({ top: -window.innerHeight, behavior: "smooth" });
132
+ console.log("Paged up");
133
+ info.scrolled = true;
134
+ info.direction = "page up";
135
+ performed = true;
136
+ break;
137
+ }
138
+ case "page_down": {
139
+ window.scrollBy({ top: window.innerHeight, behavior: "smooth" });
140
+ console.log("Paged down");
141
+ info.scrolled = true;
142
+ info.direction = "page down";
143
+ performed = true;
144
+ break;
145
+ }
146
+ case "scroll_top": {
147
+ window.scrollTo({ top: 0, behavior: "smooth" });
148
+ console.log("Scrolled to top");
149
+ info.scrolled = true;
150
+ info.direction = "top";
151
+ performed = true;
152
+ break;
153
+ }
154
+ case "scroll_bottom": {
155
+ window.scrollTo({
156
+ top: document.documentElement.scrollHeight,
157
+ behavior: "smooth",
158
+ });
159
+ console.log("Scrolled to bottom");
160
+ info.scrolled = true;
161
+ info.direction = "bottom";
162
+ performed = true;
163
+ break;
164
+ }
165
+ case "go_back": {
166
+ if (typeof window !== "undefined" && window.history.length > 1) {
167
+ notifyNavigationIntent(context);
168
+ window.history.back();
169
+ console.log("Navigated back");
170
+ info.navigated = true;
171
+ performed = true;
172
+ }
173
+ break;
174
+ }
175
+ case "go_forward": {
176
+ notifyNavigationIntent(context);
177
+ window.history.forward();
178
+ console.log("Navigated forward");
179
+ info.navigated = true;
180
+ performed = true;
181
+ break;
182
+ }
183
+ case "reload_page": {
184
+ notifyNavigationIntent(context, window.location.href);
185
+ window.location.reload();
186
+ console.log("Page reloaded");
187
+ info.reloaded = true;
188
+ performed = true;
189
+ break;
190
+ }
191
+ case "print_page": {
192
+ window.print();
193
+ console.log("Print dialog opened");
194
+ info.printed = true;
195
+ performed = true;
196
+ break;
197
+ }
198
+ case "copy_url": {
199
+ if (navigator.clipboard && window.isSecureContext) {
200
+ navigator.clipboard
201
+ .writeText(window.location.href)
202
+ .then(() => console.log("Page URL copied to clipboard"))
203
+ .catch((error) => {
204
+ console.warn("Failed to copy URL via clipboard API", error);
205
+ });
206
+ info.copied = true;
207
+ performed = true;
208
+ }
209
+ else {
210
+ try {
211
+ const textarea = document.createElement("textarea");
212
+ textarea.value = window.location.href;
213
+ textarea.style.position = "fixed";
214
+ textarea.style.opacity = "0";
215
+ document.body.appendChild(textarea);
216
+ textarea.focus();
217
+ textarea.select();
218
+ const copied = document.execCommand("copy");
219
+ document.body.removeChild(textarea);
220
+ if (copied) {
221
+ console.log("Page URL copied using fallback method");
222
+ info.copied = true;
223
+ performed = true;
224
+ }
225
+ }
226
+ catch (error) {
227
+ console.warn("Failed to copy URL", error);
228
+ }
229
+ }
230
+ break;
231
+ }
232
+ case "open_menu": {
233
+ const menuButton = document.querySelector('[aria-label*="menu" i], [aria-label*="navigation" i], .menu-button, .hamburger');
234
+ if (menuButton) {
235
+ menuButton.click();
236
+ console.log("Opened menu");
237
+ info.clicked = true;
238
+ performed = true;
239
+ }
240
+ break;
241
+ }
242
+ case "close_menu": {
243
+ const closeButton = document.querySelector('[aria-label*="close" i], .close-button, .modal-close');
244
+ if (closeButton) {
245
+ closeButton.click();
246
+ console.log("Closed menu");
247
+ info.clicked = true;
248
+ performed = true;
249
+ }
250
+ break;
251
+ }
252
+ case "focus_search": {
253
+ const searchElement = document.querySelector('input[type="search"], input[name*="search" i], input[aria-label*="search" i], input[placeholder*="search" i], [role="search"] input');
254
+ if (searchElement instanceof HTMLInputElement) {
255
+ searchElement.focus({ preventScroll: false });
256
+ searchElement.select();
257
+ console.log("Focused on search input");
258
+ info.focused = true;
259
+ performed = true;
260
+ }
261
+ break;
262
+ }
263
+ case "toggle_fullscreen": {
264
+ const element = document.fullscreenElement;
265
+ if (element) {
266
+ document
267
+ .exitFullscreen()
268
+ .then(() => console.log("Exited fullscreen"))
269
+ .catch((error) => console.warn("Failed to exit fullscreen", error));
270
+ info.fullscreen = false;
271
+ performed = true;
272
+ }
273
+ else if (document.documentElement.requestFullscreen) {
274
+ document.documentElement
275
+ .requestFullscreen()
276
+ .then(() => console.log("Entered fullscreen"))
277
+ .catch((error) => console.warn("Failed to enter fullscreen", error));
278
+ info.fullscreen = true;
279
+ performed = true;
280
+ }
281
+ break;
282
+ }
283
+ case "exit_fullscreen": {
284
+ if (document.fullscreenElement) {
285
+ document
286
+ .exitFullscreen()
287
+ .then(() => console.log("Exited fullscreen"))
288
+ .catch((error) => console.warn("Failed to exit fullscreen", error));
289
+ info.fullscreen = false;
290
+ performed = true;
291
+ }
292
+ break;
293
+ }
294
+ case "play_media": {
295
+ const media = Array.from(document.querySelectorAll("video, audio")).find((item) => item.paused);
296
+ if (media) {
297
+ void media
298
+ .play()
299
+ .then(() => console.log("Media playback started"))
300
+ .catch((error) => console.warn("Failed to play media", error));
301
+ info.mediaState = "playing";
302
+ performed = true;
303
+ }
304
+ break;
305
+ }
306
+ case "pause_media": {
307
+ const media = Array.from(document.querySelectorAll("video, audio")).find((item) => !item.paused);
308
+ if (media) {
309
+ media.pause();
310
+ console.log("Media playback paused");
311
+ info.mediaState = "paused";
312
+ performed = true;
313
+ }
314
+ break;
315
+ }
316
+ case "mute_media": {
317
+ const media = Array.from(document.querySelectorAll("video, audio"));
318
+ if (media.length > 0) {
319
+ media.forEach((item) => {
320
+ item.muted = true;
321
+ });
322
+ console.log("Media muted");
323
+ info.muted = true;
324
+ performed = true;
325
+ }
326
+ break;
327
+ }
328
+ case "unmute_media": {
329
+ const media = Array.from(document.querySelectorAll("video, audio"));
330
+ if (media.length > 0) {
331
+ media.forEach((item) => {
332
+ item.muted = false;
333
+ });
334
+ console.log("Media unmuted");
335
+ info.muted = false;
336
+ performed = true;
337
+ }
338
+ break;
339
+ }
340
+ case "navigate_home": {
341
+ notifyNavigationIntent(context, "/");
342
+ window.location.href = "/";
343
+ console.log("Navigating to home");
344
+ info.navigated = true;
345
+ performed = true;
346
+ break;
347
+ }
348
+ case "search_content": {
349
+ console.log("Vector search action delegated to controller");
350
+ info.searchPending = true;
351
+ performed = false;
352
+ break;
353
+ }
354
+ case "navigate_search": {
355
+ notifyNavigationIntent(context, "/search");
356
+ window.location.href = "/search";
357
+ console.log("Navigating to search");
358
+ info.navigated = true;
359
+ performed = true;
360
+ break;
361
+ }
362
+ case "navigate_faqs": {
363
+ notifyNavigationIntent(context, "/faqs");
364
+ window.location.href = "/faqs";
365
+ console.log("Navigating to FAQs");
366
+ info.navigated = true;
367
+ performed = true;
368
+ break;
369
+ }
370
+ case "navigate_help": {
371
+ notifyNavigationIntent(context, "/help");
372
+ window.location.href = "/help";
373
+ console.log("Navigating to help");
374
+ info.navigated = true;
375
+ performed = true;
376
+ break;
377
+ }
378
+ case "navigate_contact": {
379
+ notifyNavigationIntent(context, "/contact");
380
+ window.location.href = "/contact";
381
+ console.log("Navigating to contact");
382
+ info.navigated = true;
383
+ performed = true;
384
+ break;
385
+ }
386
+ case "navigate_about": {
387
+ notifyNavigationIntent(context, "/about");
388
+ window.location.href = "/about";
389
+ console.log("Navigating to about");
390
+ info.navigated = true;
391
+ performed = true;
392
+ break;
393
+ }
394
+ case "navigate_screen_reader": {
395
+ notifyNavigationIntent(context, "/screen-reader-access");
396
+ window.location.href = "/screen-reader-access";
397
+ console.log("Navigating to screen reader");
398
+ info.navigated = true;
399
+ performed = true;
400
+ break;
401
+ }
402
+ case "navigate_accessibility": {
403
+ notifyNavigationIntent(context, "/accessibility");
404
+ window.location.href = "/accessibility";
405
+ console.log("Navigating to accessibility");
406
+ info.navigated = true;
407
+ performed = true;
408
+ break;
409
+ }
410
+ case "navigate_disclaimer": {
411
+ notifyNavigationIntent(context, "/disclaimer");
412
+ window.location.href = "/disclaimer";
413
+ console.log("Navigating to disclaimer");
414
+ info.navigated = true;
415
+ performed = true;
416
+ break;
417
+ }
418
+ case "navigate_terms_conditions": {
419
+ notifyNavigationIntent(context, "/terms-and-conditions");
420
+ window.location.href = "/terms-and-conditions";
421
+ console.log("Navigating to terms and conditions");
422
+ info.navigated = true;
423
+ performed = true;
424
+ break;
425
+ }
426
+ case "stop": {
427
+ console.log("Stop command received");
428
+ if (onStop) {
429
+ onStop();
430
+ }
431
+ performed = true;
432
+ break;
433
+ }
434
+ default: {
435
+ console.log(`Unknown action: ${action}`);
436
+ performed = false;
437
+ break;
438
+ }
439
+ }
440
+ if (performed) {
441
+ console.log(`Action "${action}" performed successfully`);
442
+ }
443
+ return { performed, info };
444
+ };
445
+ export const extractAgentAction = (result) => {
446
+ if (!result) {
447
+ return null;
448
+ }
449
+ let raw = "";
450
+ console.log("Extracting action from result:", result, typeof result);
451
+ if (typeof result === "string") {
452
+ raw = result;
453
+ }
454
+ else if (result.content && Array.isArray(result.content)) {
455
+ raw = result.content[0]?.text || "";
456
+ }
457
+ else if (result.text) {
458
+ raw = result.text;
459
+ }
460
+ else if (typeof result === "object" && result?.action) {
461
+ raw = result?.action;
462
+ }
463
+ console.log("raw reslt", raw, result);
464
+ if (!raw) {
465
+ return null;
466
+ }
467
+ try {
468
+ const parsed = JSON.parse(raw);
469
+ return parsed;
470
+ }
471
+ catch (error) {
472
+ console.error("Failed to parse agent action:", error);
473
+ return { action: raw };
474
+ }
475
+ };
476
+ export const isAllowedAction = (action) => {
477
+ return ALLOWED_ACTIONS.has(action);
478
+ };
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_VECTOR_SEARCH_API_PATH = "/api/voice-navigation/vector-search";
2
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,8BAA8B,wCAAwC,CAAC"}
@@ -0,0 +1 @@
1
+ export const DEFAULT_VECTOR_SEARCH_API_PATH = "/api/voice-navigation/vector-search";
@@ -0,0 +1,11 @@
1
+ export { VoiceNavigationController } from "./navigation-controller.js";
2
+ export { MicrophoneHandler } from "./microphone-handler.js";
3
+ export { AzureSpeechService } from "./services/azure-speech.js";
4
+ export { BedrockService } from "./services/bedrock.js";
5
+ export * from "./types.js";
6
+ export * from "./actions.js";
7
+ export * from "./ui.js";
8
+ import { VoiceNavigationController } from "./navigation-controller.js";
9
+ import type { NavigationConfig } from "./types.js";
10
+ export declare function initNavigationOnMicrophone(config: NavigationConfig): VoiceNavigationController;
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,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;AA2JnD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,gBAAgB,GACvB,yBAAyB,CA0E3B"}
package/dist/index.js ADDED
@@ -0,0 +1,156 @@
1
+ export { VoiceNavigationController } from "./navigation-controller.js";
2
+ export { MicrophoneHandler } from "./microphone-handler.js";
3
+ export { AzureSpeechService } from "./services/azure-speech.js";
4
+ export { BedrockService } from "./services/bedrock.js";
5
+ export * from "./types.js";
6
+ export * from "./actions.js";
7
+ export * from "./ui.js";
8
+ import { VoiceNavigationController } from "./navigation-controller.js";
9
+ const CONFIG_STORAGE_KEY = "__voice_navigation_config";
10
+ const resolveOpenSearchConfig = (config) => {
11
+ if (!config) {
12
+ return undefined;
13
+ }
14
+ const alias = config.openSearch;
15
+ const resolved = config.opensearch ?? alias;
16
+ if (!resolved || typeof resolved !== "object") {
17
+ return undefined;
18
+ }
19
+ return resolved;
20
+ };
21
+ const normalizeNavigationConfig = (config) => ({
22
+ ...config,
23
+ opensearch: resolveOpenSearchConfig(config),
24
+ });
25
+ const buildPersistableConfig = (config) => {
26
+ const normalizedConfig = normalizeNavigationConfig(config);
27
+ const hasCustomHandlers = Boolean(normalizedConfig.actionHandlers &&
28
+ Object.keys(normalizedConfig.actionHandlers).length > 0);
29
+ const persistable = {
30
+ aws: normalizedConfig.aws ? { ...normalizedConfig.aws } : undefined,
31
+ azure: normalizedConfig.azure ? { ...normalizedConfig.azure } : undefined,
32
+ opensearch: normalizedConfig.opensearch
33
+ ? { ...normalizedConfig.opensearch }
34
+ : undefined,
35
+ language: normalizedConfig.language,
36
+ autoStart: normalizedConfig.autoStart,
37
+ };
38
+ if (hasCustomHandlers) {
39
+ persistable.hasCustomHandlers = true;
40
+ }
41
+ return persistable;
42
+ };
43
+ const readPersistedConfig = () => {
44
+ if (typeof window === "undefined") {
45
+ return null;
46
+ }
47
+ if (typeof sessionStorage === "undefined") {
48
+ return null;
49
+ }
50
+ try {
51
+ const raw = sessionStorage.getItem(CONFIG_STORAGE_KEY);
52
+ if (!raw) {
53
+ return null;
54
+ }
55
+ const parsed = JSON.parse(raw);
56
+ if (parsed.hasCustomHandlers) {
57
+ console.warn("Voice navigation custom action handlers are not restored automatically. Re-register them on each page load.");
58
+ }
59
+ const { hasCustomHandlers: _ignored, ...rest } = parsed;
60
+ return rest;
61
+ }
62
+ catch (error) {
63
+ console.warn("Failed to read persisted voice navigation config:", error);
64
+ return null;
65
+ }
66
+ };
67
+ const describeOpenSearchConfig = (config) => {
68
+ if (!config) {
69
+ return "";
70
+ }
71
+ const { node, username, password, index, vectorField, size, minScore, numCandidates, sourceFields, ssl, apiPath, } = config;
72
+ return JSON.stringify({
73
+ node,
74
+ username,
75
+ password,
76
+ index,
77
+ vectorField,
78
+ size,
79
+ minScore,
80
+ numCandidates,
81
+ sourceFields,
82
+ ssl,
83
+ apiPath,
84
+ });
85
+ };
86
+ export function initNavigationOnMicrophone(config) {
87
+ if (typeof window === "undefined" || typeof document === "undefined") {
88
+ throw new Error("Voice navigation requires a browser environment");
89
+ }
90
+ const globalWindow = window;
91
+ const normalizedConfig = normalizeNavigationConfig(config);
92
+ const previousConfigRaw = globalWindow.__voiceNavigationConfig;
93
+ const previousConfig = previousConfigRaw
94
+ ? normalizeNavigationConfig(previousConfigRaw)
95
+ : undefined;
96
+ globalWindow.__voiceNavigationConfig = normalizedConfig;
97
+ try {
98
+ if (typeof sessionStorage !== "undefined") {
99
+ const persistable = buildPersistableConfig(normalizedConfig);
100
+ sessionStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(persistable));
101
+ }
102
+ }
103
+ catch (error) {
104
+ console.warn("Failed to persist voice navigation config:", error);
105
+ }
106
+ if (globalWindow.__navigateOnMicrophoneInitialized) {
107
+ const existingController = globalWindow.navigationController;
108
+ const previousOpenSearchConfig = resolveOpenSearchConfig(previousConfig);
109
+ const currentOpenSearchConfig = resolveOpenSearchConfig(normalizedConfig);
110
+ const hadVectorSearch = Boolean(previousOpenSearchConfig);
111
+ const wantsVectorSearch = Boolean(currentOpenSearchConfig);
112
+ const existingHasVectorSearch = Boolean(existingController?.vectorSearchService);
113
+ const vectorConfigChanged = hadVectorSearch &&
114
+ wantsVectorSearch &&
115
+ describeOpenSearchConfig(previousOpenSearchConfig) !==
116
+ describeOpenSearchConfig(currentOpenSearchConfig);
117
+ if (wantsVectorSearch &&
118
+ (!hadVectorSearch || vectorConfigChanged || !existingHasVectorSearch)) {
119
+ try {
120
+ existingController?.destroy?.();
121
+ }
122
+ catch (disposeError) {
123
+ console.warn("Failed to dispose existing voice navigation controller:", disposeError);
124
+ }
125
+ globalWindow.__navigateOnMicrophoneInitialized = false;
126
+ globalWindow.navigationController = undefined;
127
+ }
128
+ else {
129
+ console.warn("Voice navigation already initialized");
130
+ return existingController;
131
+ }
132
+ }
133
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
134
+ throw new Error("Microphone access is not supported in this browser");
135
+ }
136
+ globalWindow.__navigateOnMicrophoneInitialized = true;
137
+ const controller = new VoiceNavigationController(normalizedConfig);
138
+ globalWindow.navigationController = controller;
139
+ return controller;
140
+ }
141
+ if (typeof window !== "undefined") {
142
+ const globalWindow = window;
143
+ const autoConfigRaw = globalWindow.__voiceNavigationConfig || readPersistedConfig();
144
+ if (autoConfigRaw) {
145
+ const autoConfig = normalizeNavigationConfig(autoConfigRaw);
146
+ try {
147
+ if (!globalWindow.__voiceNavigationConfig) {
148
+ globalWindow.__voiceNavigationConfig = autoConfig;
149
+ }
150
+ initNavigationOnMicrophone(autoConfig);
151
+ }
152
+ catch (error) {
153
+ console.error("Failed to auto-initialize voice navigation:", error);
154
+ }
155
+ }
156
+ }