@navai/voice-frontend 0.1.2 → 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/dist/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ import {
2
+ Orb,
3
+ useNavaiVoiceOrbStyles
4
+ } from "./chunk-KBBRQQLK.js";
5
+
1
6
  // src/agent.ts
2
7
  import { RealtimeAgent, tool } from "@openai/agents/realtime";
3
8
  import { z } from "zod";
@@ -242,27 +247,33 @@ function getNavaiRoutePromptLines(routes = []) {
242
247
  // src/agent.ts
243
248
  var RESERVED_TOOL_NAMES = /* @__PURE__ */ new Set(["navigate_to", "execute_app_function"]);
244
249
  var TOOL_NAME_REGEXP = /^[a-zA-Z0-9_-]{1,64}$/;
250
+ var DEBUG_PREFIX = "[navai debug]";
245
251
  function toErrorMessage2(error) {
246
252
  return error instanceof Error ? error.message : String(error);
247
253
  }
248
- async function buildNavaiAgent(options) {
249
- const functionsRegistry = await loadNavaiFunctions(options.functionModuleLoaders ?? {});
250
- const backendWarnings = [];
254
+ function debugLog(message, details) {
255
+ if (details === void 0) {
256
+ console.log(`${DEBUG_PREFIX} ${message}`);
257
+ return;
258
+ }
259
+ console.log(`${DEBUG_PREFIX} ${message}`, details);
260
+ }
261
+ function normalizeBackendFunctions(backendFunctions, functionsRegistry, warnings) {
251
262
  const backendFunctionsByName = /* @__PURE__ */ new Map();
252
263
  const backendFunctionsOrdered = [];
253
- for (const backendFunction of options.backendFunctions ?? []) {
264
+ for (const backendFunction of backendFunctions ?? []) {
254
265
  const name = backendFunction.name.trim().toLowerCase();
255
266
  if (!name) {
256
267
  continue;
257
268
  }
258
269
  if (functionsRegistry.byName.has(name)) {
259
- backendWarnings.push(
270
+ warnings.push(
260
271
  `[navai] Ignored backend function "${backendFunction.name}": name conflicts with a frontend function.`
261
272
  );
262
273
  continue;
263
274
  }
264
275
  if (backendFunctionsByName.has(name)) {
265
- backendWarnings.push(`[navai] Ignored duplicated backend function "${backendFunction.name}".`);
276
+ warnings.push(`[navai] Ignored duplicated backend function "${backendFunction.name}".`);
266
277
  continue;
267
278
  }
268
279
  const normalizedDefinition = {
@@ -272,94 +283,119 @@ async function buildNavaiAgent(options) {
272
283
  backendFunctionsByName.set(name, normalizedDefinition);
273
284
  backendFunctionsOrdered.push(normalizedDefinition);
274
285
  }
286
+ return backendFunctionsOrdered;
287
+ }
288
+ function createExecuteAppFunction(input) {
289
+ const backendFunctionsByName = new Map(input.backendFunctions.map((item) => [item.name, item]));
275
290
  const availableFunctionNames = [
276
- ...functionsRegistry.ordered.map((item) => item.name),
277
- ...backendFunctionsOrdered.map((item) => item.name)
291
+ ...input.functionsRegistry.ordered.map((item) => item.name),
292
+ ...input.backendFunctions.map((item) => item.name)
278
293
  ];
279
- const aliasWarnings = [];
280
- const directFunctionToolNames = [...new Set(availableFunctionNames)].map((name) => name.trim().toLowerCase()).filter((name) => {
281
- if (!name) {
282
- return false;
283
- }
284
- if (RESERVED_TOOL_NAMES.has(name)) {
285
- aliasWarnings.push(
286
- `[navai] Function "${name}" is available only via execute_app_function because its name conflicts with a built-in tool.`
287
- );
288
- return false;
289
- }
290
- if (!TOOL_NAME_REGEXP.test(name)) {
291
- aliasWarnings.push(
292
- `[navai] Function "${name}" is available only via execute_app_function because its name is not a valid tool id.`
293
- );
294
- return false;
295
- }
296
- return true;
297
- });
298
294
  const executeAppFunction = async (requestedName, payload) => {
299
295
  const requested = requestedName.trim().toLowerCase();
300
- const frontendDefinition = functionsRegistry.byName.get(requested);
296
+ debugLog("execute_app_function called", { requestedName, requested, payload });
297
+ const frontendDefinition = input.functionsRegistry.byName.get(requested);
301
298
  if (frontendDefinition) {
302
299
  try {
303
- const result = await frontendDefinition.run(payload ?? {}, options);
304
- return { ok: true, function_name: frontendDefinition.name, source: frontendDefinition.source, result };
300
+ debugLog("executing frontend function", {
301
+ functionName: frontendDefinition.name,
302
+ source: frontendDefinition.source
303
+ });
304
+ const result = await frontendDefinition.run(payload ?? {}, input.context);
305
+ const response = {
306
+ ok: true,
307
+ function_name: frontendDefinition.name,
308
+ source: frontendDefinition.source,
309
+ result
310
+ };
311
+ debugLog("frontend function completed", response);
312
+ return response;
305
313
  } catch (error) {
306
- return {
314
+ const failure = {
307
315
  ok: false,
308
316
  function_name: frontendDefinition.name,
309
317
  error: "Function execution failed.",
310
318
  details: toErrorMessage2(error)
311
319
  };
320
+ debugLog("frontend function failed", failure);
321
+ return failure;
312
322
  }
313
323
  }
314
324
  const backendDefinition = backendFunctionsByName.get(requested);
315
325
  if (!backendDefinition) {
316
- return {
326
+ const failure = {
317
327
  ok: false,
318
328
  error: "Unknown or disallowed function.",
319
329
  available_functions: availableFunctionNames
320
330
  };
331
+ debugLog("execute_app_function rejected unknown function", failure);
332
+ return failure;
321
333
  }
322
- if (!options.executeBackendFunction) {
323
- return {
334
+ if (!input.executeBackendFunction) {
335
+ const failure = {
324
336
  ok: false,
325
337
  function_name: backendDefinition.name,
326
338
  error: "Backend function execution is not configured."
327
339
  };
340
+ debugLog("backend function execution unavailable", failure);
341
+ return failure;
328
342
  }
329
343
  try {
330
- const result = await options.executeBackendFunction({
344
+ debugLog("executing backend function", {
345
+ functionName: backendDefinition.name,
346
+ source: backendDefinition.source ?? "backend"
347
+ });
348
+ const result = await input.executeBackendFunction({
331
349
  functionName: backendDefinition.name,
332
350
  payload: payload ?? null
333
351
  });
334
- return {
352
+ const response = {
335
353
  ok: true,
336
354
  function_name: backendDefinition.name,
337
355
  source: backendDefinition.source ?? "backend",
338
356
  result
339
357
  };
358
+ debugLog("backend function completed", response);
359
+ return response;
340
360
  } catch (error) {
341
- return {
361
+ const failure = {
342
362
  ok: false,
343
363
  function_name: backendDefinition.name,
344
364
  error: "Function execution failed.",
345
365
  details: toErrorMessage2(error)
346
366
  };
367
+ debugLog("backend function failed", failure);
368
+ return failure;
347
369
  }
348
370
  };
349
- const navigateTool = tool({
350
- name: "navigate_to",
351
- description: "Navigate to an allowed route in the current app.",
352
- parameters: z.object({
353
- target: z.string().min(1).describe("Route name or route path. Example: perfil, ajustes, /profile, /settings")
354
- }),
355
- execute: async ({ target }) => {
356
- const path = resolveNavaiRoute(target, options.routes);
357
- if (!path) {
358
- return { ok: false, error: "Unknown or disallowed route." };
359
- }
360
- options.navigate(path);
361
- return { ok: true, path };
371
+ return {
372
+ availableFunctionNames,
373
+ executeAppFunction
374
+ };
375
+ }
376
+ function createFunctionTools(input) {
377
+ const aliasWarnings = [];
378
+ const availableFunctionNames = [
379
+ ...input.functionsRegistry.ordered.map((item) => item.name),
380
+ ...input.backendFunctions.map((item) => item.name)
381
+ ];
382
+ const directFunctionToolNames = input.includeDirectAliases === false ? [] : [...new Set(availableFunctionNames)].map((name) => name.trim().toLowerCase()).filter((name) => {
383
+ if (!name) {
384
+ return false;
362
385
  }
386
+ if (RESERVED_TOOL_NAMES.has(name)) {
387
+ aliasWarnings.push(
388
+ `[navai] Function "${name}" is available only via execute_app_function because its name conflicts with a built-in tool.`
389
+ );
390
+ return false;
391
+ }
392
+ if (!TOOL_NAME_REGEXP.test(name)) {
393
+ aliasWarnings.push(
394
+ `[navai] Function "${name}" is available only via execute_app_function because its name is not a valid tool id.`
395
+ );
396
+ return false;
397
+ }
398
+ return true;
363
399
  });
364
400
  const executeFunctionTool = tool({
365
401
  name: "execute_app_function",
@@ -370,36 +406,146 @@ async function buildNavaiAgent(options) {
370
406
  "Payload object. Use null when no arguments are needed. Use payload.args as array for function args, payload.constructorArgs for class constructors, payload.methodArgs for class methods."
371
407
  )
372
408
  }),
373
- execute: async ({ function_name, payload }) => await executeAppFunction(function_name, payload)
409
+ execute: async ({ function_name, payload }) => await input.executeAppFunction(function_name, payload)
374
410
  });
375
411
  const directFunctionTools = directFunctionToolNames.map(
376
412
  (functionName) => tool({
377
413
  name: functionName,
378
414
  description: `Direct alias for execute_app_function("${functionName}").`,
379
415
  parameters: z.object({
380
- payload: z.record(z.string(), z.unknown()).nullable().optional().describe(
416
+ payload: z.record(z.string(), z.unknown()).nullable().describe(
381
417
  "Payload object. Optional. Use payload.args as array for function args, payload.constructorArgs for class constructors, payload.methodArgs for class methods."
382
418
  )
383
419
  }),
384
- execute: async ({ payload }) => await executeAppFunction(functionName, payload ?? null)
420
+ execute: async ({ payload }) => await input.executeAppFunction(functionName, payload ?? null)
385
421
  })
386
422
  );
387
- const routeLines = getNavaiRoutePromptLines(options.routes);
388
- const functionLines = functionsRegistry.ordered.length + backendFunctionsOrdered.length > 0 ? [
423
+ return {
424
+ aliasWarnings,
425
+ availableFunctionNames,
426
+ executeFunctionTool,
427
+ directFunctionTools
428
+ };
429
+ }
430
+ function buildFunctionLines(functionsRegistry, backendFunctions) {
431
+ return functionsRegistry.ordered.length + backendFunctions.length > 0 ? [
389
432
  ...functionsRegistry.ordered.map((item) => `- ${item.name}: ${item.description}`),
390
- ...backendFunctionsOrdered.map(
391
- (item) => `- ${item.name}: ${item.description ?? "Execute backend function."}`
392
- )
433
+ ...backendFunctions.map((item) => `- ${item.name}: ${item.description ?? "Execute backend function."}`)
393
434
  ] : ["- none"];
435
+ }
436
+ async function buildNavaiAgent(options) {
437
+ const aggregatedWarnings = [];
438
+ const configuredAgents = (options.agents ?? []).filter(
439
+ (agent2) => Object.keys(agent2.functionModuleLoaders ?? {}).length > 0
440
+ );
441
+ const primaryAgentConfig = configuredAgents.find((agent2) => agent2.key === options.primaryAgentKey) ?? configuredAgents.find((agent2) => agent2.isPrimary) ?? configuredAgents[0];
442
+ const primaryFunctionLoaders = primaryAgentConfig?.functionModuleLoaders ?? options.functionModuleLoaders ?? {};
443
+ const functionsRegistry = await loadNavaiFunctions(primaryFunctionLoaders);
444
+ const backendFunctionsOrdered = normalizeBackendFunctions(options.backendFunctions, functionsRegistry, aggregatedWarnings);
445
+ const primaryExecutionSurface = createExecuteAppFunction({
446
+ functionsRegistry,
447
+ backendFunctions: backendFunctionsOrdered,
448
+ executeBackendFunction: options.executeBackendFunction,
449
+ context: options
450
+ });
451
+ const primaryFunctionTools = createFunctionTools({
452
+ functionsRegistry,
453
+ backendFunctions: backendFunctionsOrdered,
454
+ executeAppFunction: primaryExecutionSurface.executeAppFunction
455
+ });
456
+ aggregatedWarnings.push(...functionsRegistry.warnings, ...primaryFunctionTools.aliasWarnings);
457
+ const navigateTool = tool({
458
+ name: "navigate_to",
459
+ description: "Navigate to an allowed route in the current app.",
460
+ parameters: z.object({
461
+ target: z.string().min(1).describe("Route name or route path. Example: perfil, ajustes, /profile, /settings")
462
+ }),
463
+ execute: async ({ target }) => {
464
+ debugLog("navigate_to called", { target });
465
+ const path = resolveNavaiRoute(target, options.routes);
466
+ if (!path) {
467
+ const failure = { ok: false, error: "Unknown or disallowed route." };
468
+ debugLog("navigate_to rejected", failure);
469
+ return failure;
470
+ }
471
+ options.navigate(path);
472
+ const response = { ok: true, path };
473
+ debugLog("navigate_to completed", response);
474
+ return response;
475
+ }
476
+ });
477
+ const routeLines = getNavaiRoutePromptLines(options.routes);
478
+ const functionLines = buildFunctionLines(functionsRegistry, backendFunctionsOrdered);
479
+ const specialistAgents = [];
480
+ const specialistLines = [];
481
+ for (const runtimeAgent of configuredAgents) {
482
+ if (primaryAgentConfig && runtimeAgent.key === primaryAgentConfig.key) {
483
+ continue;
484
+ }
485
+ const specialistRegistry = await loadNavaiFunctions(runtimeAgent.functionModuleLoaders);
486
+ const specialistWarnings = [...specialistRegistry.warnings];
487
+ const specialistBackendFunctions = normalizeBackendFunctions(
488
+ options.backendFunctions,
489
+ specialistRegistry,
490
+ specialistWarnings
491
+ );
492
+ const specialistExecutionSurface = createExecuteAppFunction({
493
+ functionsRegistry: specialistRegistry,
494
+ backendFunctions: specialistBackendFunctions,
495
+ executeBackendFunction: options.executeBackendFunction,
496
+ context: options
497
+ });
498
+ const specialistFunctionTools = createFunctionTools({
499
+ functionsRegistry: specialistRegistry,
500
+ backendFunctions: specialistBackendFunctions,
501
+ executeAppFunction: specialistExecutionSurface.executeAppFunction,
502
+ includeDirectAliases: false
503
+ });
504
+ specialistWarnings.push(...specialistFunctionTools.aliasWarnings);
505
+ aggregatedWarnings.push(...specialistWarnings);
506
+ const specialistInstructions = [
507
+ runtimeAgent.instructions ?? `You are the ${runtimeAgent.name} specialist agent for this web app.`,
508
+ "Allowed app functions:",
509
+ ...buildFunctionLines(specialistRegistry, specialistBackendFunctions),
510
+ "Rules:",
511
+ "- Always use execute_app_function for app actions.",
512
+ "- When no arguments are needed, call execute_app_function with payload set to null.",
513
+ "- Use only the functions available to this specialist agent.",
514
+ "- Do not navigate unless one of your allowed functions explicitly does so.",
515
+ "- Return a concise result to the main NAVAI agent."
516
+ ].join("\n");
517
+ debugLog("creating specialist agent", {
518
+ key: runtimeAgent.key,
519
+ name: runtimeAgent.name,
520
+ functions: [
521
+ ...specialistRegistry.ordered.map((item) => item.name),
522
+ ...specialistBackendFunctions.map((item) => item.name)
523
+ ]
524
+ });
525
+ const specialistAgent = new RealtimeAgent({
526
+ name: runtimeAgent.name,
527
+ handoffDescription: runtimeAgent.handoffDescription ?? runtimeAgent.description ?? `Delegate specialist work to ${runtimeAgent.name}.`,
528
+ instructions: specialistInstructions,
529
+ tools: [specialistFunctionTools.executeFunctionTool]
530
+ });
531
+ specialistAgents.push(specialistAgent);
532
+ specialistLines.push(
533
+ `- ${runtimeAgent.name}: ${runtimeAgent.description ?? runtimeAgent.handoffDescription ?? "Specialist agent available by delegation."}`
534
+ );
535
+ }
394
536
  const instructions = [
395
- options.baseInstructions ?? "You are a voice assistant embedded in a web app.",
537
+ primaryAgentConfig?.instructions ?? options.baseInstructions ?? "You are the main NAVAI voice agent embedded in a web app.",
396
538
  "Allowed routes:",
397
539
  ...routeLines,
398
540
  "Allowed app functions:",
399
541
  ...functionLines,
542
+ "Available specialist agents:",
543
+ ...specialistLines.length > 0 ? specialistLines : ["- none"],
400
544
  "Rules:",
401
545
  "- If user asks to go/open a section, always call navigate_to.",
402
- "- If user asks to run an internal action, call execute_app_function or the matching direct function tool.",
546
+ "- If user asks to run an internal action that belongs to you, call execute_app_function or the matching direct function tool.",
547
+ "- If the task clearly belongs to a specialist agent, hand off to that specialist agent.",
548
+ "- Food recommendations, fast food, hamburgers, pizza, tacos, snacks, and meal suggestions belong to the food specialist.",
403
549
  "- Always include payload in execute_app_function. Use null when no arguments are needed.",
404
550
  "- For execute_app_function, pass arguments using payload.args (array).",
405
551
  "- For class methods, pass payload.constructorArgs and payload.methodArgs.",
@@ -407,11 +553,23 @@ async function buildNavaiAgent(options) {
407
553
  "- If destination/action is unclear, ask a brief clarifying question."
408
554
  ].join("\n");
409
555
  const agent = new RealtimeAgent({
410
- name: options.agentName ?? "Navai Voice Agent",
556
+ name: primaryAgentConfig?.name ?? options.agentName ?? "Navai Voice Agent",
411
557
  instructions,
412
- tools: [navigateTool, executeFunctionTool, ...directFunctionTools]
558
+ handoffs: specialistAgents,
559
+ tools: [
560
+ navigateTool,
561
+ primaryFunctionTools.executeFunctionTool,
562
+ ...primaryFunctionTools.directFunctionTools
563
+ ]
564
+ });
565
+ debugLog("created primary agent", {
566
+ name: primaryAgentConfig?.name ?? options.agentName ?? "Navai Voice Agent",
567
+ primaryAgentKey: primaryAgentConfig?.key ?? null,
568
+ directFunctions: functionsRegistry.ordered.map((item) => item.name),
569
+ backendFunctions: backendFunctionsOrdered.map((item) => item.name),
570
+ specialistDelegates: configuredAgents.filter((runtimeAgent) => runtimeAgent.key !== primaryAgentConfig?.key).map((runtimeAgent) => runtimeAgent.key)
413
571
  });
414
- return { agent, warnings: [...functionsRegistry.warnings, ...backendWarnings, ...aliasWarnings] };
572
+ return { agent, warnings: aggregatedWarnings };
415
573
  }
416
574
 
417
575
  // src/backend.ts
@@ -534,6 +692,7 @@ function createNavaiBackendClient(options = {}) {
534
692
  // src/runtime.ts
535
693
  var ROUTES_ENV_KEYS = ["NAVAI_ROUTES_FILE"];
536
694
  var FUNCTIONS_ENV_KEYS = ["NAVAI_FUNCTIONS_FOLDERS"];
695
+ var AGENTS_ENV_KEYS = ["NAVAI_AGENTS_FOLDERS"];
537
696
  var MODEL_ENV_KEYS = ["NAVAI_REALTIME_MODEL"];
538
697
  async function resolveNavaiFrontendRuntimeConfig(options) {
539
698
  const warnings = [];
@@ -543,6 +702,7 @@ async function resolveNavaiFrontendRuntimeConfig(options) {
543
702
  const defaultFunctionsFolder = options.defaultFunctionsFolder ?? "src/ai/functions-modules";
544
703
  const routesFile = readOptional2(options.routesFile) ?? readFirstOptionalEnv(options.env, ROUTES_ENV_KEYS) ?? defaultRoutesFile;
545
704
  const functionsFolders = readOptional2(options.functionsFolders) ?? readFirstOptionalEnv(options.env, FUNCTIONS_ENV_KEYS) ?? defaultFunctionsFolder;
705
+ const agentsFolders = readOptional2(options.agentsFolders) ?? readFirstOptionalEnv(options.env, AGENTS_ENV_KEYS);
546
706
  const modelOverride = readOptional2(options.modelOverride) ?? readFirstOptionalEnv(options.env, MODEL_ENV_KEYS);
547
707
  const routes = await resolveRoutes({
548
708
  routesFile,
@@ -554,12 +714,23 @@ async function resolveNavaiFrontendRuntimeConfig(options) {
554
714
  const functionModuleLoaders = resolveFunctionModuleLoaders({
555
715
  indexedLoaders,
556
716
  functionsFolders,
717
+ agentsFolders,
557
718
  defaultFunctionsFolder,
558
719
  warnings
559
720
  });
721
+ const agents = await resolveRuntimeAgents({
722
+ indexedLoaders,
723
+ functionModuleLoaders,
724
+ functionsFolders,
725
+ agentsFolders,
726
+ defaultFunctionsFolder
727
+ });
728
+ const primaryAgentKey = agents.find((agent) => agent.isPrimary)?.key;
560
729
  return {
561
730
  routes,
562
731
  functionModuleLoaders,
732
+ agents,
733
+ primaryAgentKey,
563
734
  modelOverride,
564
735
  warnings
565
736
  };
@@ -595,10 +766,11 @@ async function resolveRoutes(input) {
595
766
  }
596
767
  function resolveFunctionModuleLoaders(input) {
597
768
  const configuredTokens = input.functionsFolders.split(",").map((value) => value.trim()).filter(Boolean);
769
+ const agentFolders = parseCsvList(input.agentsFolders);
598
770
  const tokens = configuredTokens.length > 0 ? configuredTokens : [input.defaultFunctionsFolder];
599
- const matchers = tokens.map((token) => createPathMatcher(token));
771
+ const matchers = tokens.map((token) => createPathMatcher(token, agentFolders));
600
772
  const matchedEntries = input.indexedLoaders.filter(
601
- (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && matchers.some((matcher) => matcher(entry.normalizedPath))
773
+ (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && !isAgentConfigPath(entry.normalizedPath) && matchers.some((matcher) => matcher(entry.normalizedPath))
602
774
  );
603
775
  if (matchedEntries.length > 0) {
604
776
  return Object.fromEntries(matchedEntries.map((entry) => [entry.rawPath, entry.load]));
@@ -608,9 +780,9 @@ function resolveFunctionModuleLoaders(input) {
608
780
  `[navai] NAVAI_FUNCTIONS_FOLDERS did not match any module: "${input.functionsFolders}". Falling back to "${input.defaultFunctionsFolder}".`
609
781
  );
610
782
  }
611
- const fallbackMatcher = createPathMatcher(input.defaultFunctionsFolder);
783
+ const fallbackMatcherWithAgents = createPathMatcher(input.defaultFunctionsFolder, agentFolders);
612
784
  const fallbackEntries = input.indexedLoaders.filter(
613
- (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && fallbackMatcher(entry.normalizedPath)
785
+ (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && !isAgentConfigPath(entry.normalizedPath) && fallbackMatcherWithAgents(entry.normalizedPath)
614
786
  );
615
787
  return Object.fromEntries(fallbackEntries.map((entry) => [entry.rawPath, entry.load]));
616
788
  }
@@ -653,7 +825,7 @@ function buildModuleCandidates(inputPath) {
653
825
  }
654
826
  return [srcPrefixed, `${srcPrefixed}.ts`, `${srcPrefixed}.js`, `${srcPrefixed}/index.ts`, `${srcPrefixed}/index.js`];
655
827
  }
656
- function createPathMatcher(input) {
828
+ function createPathMatcher(input, agentFolders = []) {
657
829
  const raw = normalizePath(input);
658
830
  if (!raw) {
659
831
  return () => false;
@@ -671,8 +843,141 @@ function createPathMatcher(input) {
671
843
  return (path) => path === normalized;
672
844
  }
673
845
  const base = normalized.replace(/\/+$/, "");
846
+ const normalizedAgents = agentFolders.map(normalizePathSegment).filter(Boolean);
847
+ if (normalizedAgents.length > 0) {
848
+ return (path) => {
849
+ if (!path.startsWith(`${base}/`)) {
850
+ return false;
851
+ }
852
+ const suffix = path.slice(base.length + 1);
853
+ const firstSegment = suffix.split("/", 1)[0] ?? "";
854
+ return normalizedAgents.includes(firstSegment);
855
+ };
856
+ }
674
857
  return (path) => path === base || path.startsWith(`${base}/`);
675
858
  }
859
+ async function resolveRuntimeAgents(input) {
860
+ const configuredAgents = parseCsvList(input.agentsFolders);
861
+ if (configuredAgents.length === 0) {
862
+ return [];
863
+ }
864
+ const loaderByPath = new Map(input.indexedLoaders.map((entry) => [entry.normalizedPath, entry]));
865
+ const baseDirectories = resolveAgentBaseDirectories(input.functionsFolders, input.defaultFunctionsFolder);
866
+ const groupedLoaders = /* @__PURE__ */ new Map();
867
+ for (const [rawPath, load] of Object.entries(input.functionModuleLoaders)) {
868
+ const agentKey = extractAgentKeyFromPath(rawPath, baseDirectories, configuredAgents);
869
+ if (!agentKey) {
870
+ continue;
871
+ }
872
+ const current = groupedLoaders.get(agentKey) ?? {};
873
+ current[rawPath] = load;
874
+ groupedLoaders.set(agentKey, current);
875
+ }
876
+ const configuredPrimaryKey = configuredAgents[0];
877
+ const agents = [];
878
+ for (const agentKey of configuredAgents) {
879
+ const functionLoaders = groupedLoaders.get(agentKey);
880
+ if (!functionLoaders || Object.keys(functionLoaders).length === 0) {
881
+ continue;
882
+ }
883
+ const config = await loadAgentModuleConfig(agentKey, baseDirectories, loaderByPath);
884
+ agents.push({
885
+ key: config.key?.trim() || agentKey,
886
+ name: readOptional2(config.name) ?? humanizeAgentKey(agentKey),
887
+ description: readOptional2(config.description),
888
+ handoffDescription: readOptional2(config.handoffDescription) ?? readOptional2(config.description),
889
+ instructions: readOptional2(config.instructions),
890
+ isPrimary: config.isPrimary === true || agentKey === configuredPrimaryKey,
891
+ functionModuleLoaders: functionLoaders
892
+ });
893
+ }
894
+ if (agents.filter((agent) => agent.isPrimary).length === 0 && agents[0]) {
895
+ agents[0].isPrimary = true;
896
+ }
897
+ if (agents.filter((agent) => agent.isPrimary).length > 1) {
898
+ let primaryAssigned = false;
899
+ for (const agent of agents) {
900
+ if (agent.isPrimary && !primaryAssigned) {
901
+ primaryAssigned = true;
902
+ continue;
903
+ }
904
+ agent.isPrimary = false;
905
+ }
906
+ }
907
+ return agents;
908
+ }
909
+ async function loadAgentModuleConfig(agentKey, baseDirectories, loaderByPath) {
910
+ for (const baseDirectory of baseDirectories) {
911
+ const configBase = `${baseDirectory}/${agentKey}/agent.config`;
912
+ const matchedLoader = buildModuleCandidates(configBase).map((candidate) => loaderByPath.get(candidate)).find(Boolean);
913
+ if (!matchedLoader) {
914
+ continue;
915
+ }
916
+ try {
917
+ const imported = await matchedLoader.load();
918
+ return readAgentModuleConfig(imported);
919
+ } catch {
920
+ return {};
921
+ }
922
+ }
923
+ return {};
924
+ }
925
+ function readAgentModuleConfig(moduleShape) {
926
+ const candidate = readRecord(moduleShape.NAVAI_AGENT) ?? readRecord(moduleShape.agent) ?? readRecord(moduleShape.default) ?? {};
927
+ return {
928
+ key: readOptionalString(candidate.key),
929
+ name: readOptionalString(candidate.name),
930
+ description: readOptionalString(candidate.description),
931
+ handoffDescription: readOptionalString(candidate.handoffDescription),
932
+ instructions: readOptionalString(candidate.instructions),
933
+ isPrimary: candidate.isPrimary === true
934
+ };
935
+ }
936
+ function readRecord(value) {
937
+ return value && typeof value === "object" ? value : null;
938
+ }
939
+ function readOptionalString(value) {
940
+ return typeof value === "string" ? readOptional2(value) : void 0;
941
+ }
942
+ function resolveAgentBaseDirectories(functionsFolders, defaultFunctionsFolder) {
943
+ const configuredTokens = functionsFolders.split(",").map((value) => value.trim()).filter(Boolean);
944
+ const tokens = configuredTokens.length > 0 ? configuredTokens : [defaultFunctionsFolder];
945
+ return [...new Set(tokens.map(toAgentBaseDirectory).filter(Boolean))];
946
+ }
947
+ function toAgentBaseDirectory(input) {
948
+ const raw = normalizePath(input);
949
+ if (!raw) {
950
+ return null;
951
+ }
952
+ const normalized = raw.startsWith("src/") ? raw : `src/${raw}`;
953
+ if (normalized.includes("*") || /\.[cm]?[jt]s$/.test(normalized)) {
954
+ return null;
955
+ }
956
+ if (normalized.endsWith("/...")) {
957
+ return normalized.slice(0, -4).replace(/\/+$/, "") || null;
958
+ }
959
+ return normalized.replace(/\/+$/, "") || null;
960
+ }
961
+ function extractAgentKeyFromPath(pathValue, baseDirectories, configuredAgents) {
962
+ const normalized = normalizePath(pathValue);
963
+ for (const baseDirectory of baseDirectories) {
964
+ if (!normalized.startsWith(`${baseDirectory}/`)) {
965
+ continue;
966
+ }
967
+ const suffix = normalized.slice(baseDirectory.length + 1);
968
+ const firstSegment = suffix.split("/", 1)[0] ?? "";
969
+ if (configuredAgents.includes(firstSegment)) {
970
+ return firstSegment;
971
+ }
972
+ }
973
+ return void 0;
974
+ }
975
+ function humanizeAgentKey(value) {
976
+ return value.split(/[_-]+/g).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
977
+ }
978
+ function isAgentConfigPath(pathValue) {
979
+ return /\/agent\.config\.[cm]?[jt]s$/i.test(pathValue);
980
+ }
676
981
  function globToRegExp(pattern) {
677
982
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
678
983
  const wildcardSafe = escaped.replace(/\*\*/g, "___DOUBLE_STAR___");
@@ -683,6 +988,12 @@ function globToRegExp(pattern) {
683
988
  function normalizePath(input) {
684
989
  return input.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/^(\.\/)+/, "").replace(/^(\.\.\/)+/, "");
685
990
  }
991
+ function normalizePathSegment(input) {
992
+ return normalizePath(input).replace(/\//g, "");
993
+ }
994
+ function parseCsvList(input) {
995
+ return (input ?? "").split(",").map((value) => normalizePathSegment(value)).filter(Boolean);
996
+ }
686
997
  function readFirstOptionalEnv(env, keys) {
687
998
  if (!env) {
688
999
  return void 0;
@@ -713,6 +1024,7 @@ function toErrorMessage3(error) {
713
1024
  // src/useWebVoiceAgent.ts
714
1025
  import { RealtimeSession } from "@openai/agents/realtime";
715
1026
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1027
+ var DEBUG_PREFIX2 = "[navai debug]";
716
1028
  function formatError(error) {
717
1029
  if (error instanceof Error) {
718
1030
  return error.message;
@@ -726,6 +1038,13 @@ function emitWarnings(warnings) {
726
1038
  }
727
1039
  }
728
1040
  }
1041
+ function debugLog2(message, details) {
1042
+ if (details === void 0) {
1043
+ console.log(`${DEBUG_PREFIX2} ${message}`);
1044
+ return;
1045
+ }
1046
+ console.log(`${DEBUG_PREFIX2} ${message}`, details);
1047
+ }
729
1048
  function useWebVoiceAgent(options) {
730
1049
  const sessionRef = useRef(null);
731
1050
  const attachedRealtimeSessionRef = useRef(null);
@@ -736,6 +1055,7 @@ function useWebVoiceAgent(options) {
736
1055
  env: options.env,
737
1056
  routesFile: options.routesFile,
738
1057
  functionsFolders: options.functionsFolders,
1058
+ agentsFolders: options.agentsFolders,
739
1059
  modelOverride: options.modelOverride,
740
1060
  defaultRoutesFile: options.defaultRoutesFile,
741
1061
  defaultFunctionsFolder: options.defaultFunctionsFolder
@@ -744,6 +1064,7 @@ function useWebVoiceAgent(options) {
744
1064
  options.defaultFunctionsFolder,
745
1065
  options.defaultRoutes,
746
1066
  options.defaultRoutesFile,
1067
+ options.agentsFolders,
747
1068
  options.env,
748
1069
  options.functionsFolders,
749
1070
  options.modelOverride,
@@ -790,6 +1111,45 @@ function useWebVoiceAgent(options) {
790
1111
  const attachSessionAudioListeners = useCallback(
791
1112
  (session) => {
792
1113
  detachSessionAudioListeners();
1114
+ session.on("agent_start", (_context, agent, turnInput) => {
1115
+ debugLog2("session agent_start", {
1116
+ agent: agent.name,
1117
+ turnInputCount: Array.isArray(turnInput) ? turnInput.length : 0
1118
+ });
1119
+ });
1120
+ session.on("agent_end", (_context, agent, output) => {
1121
+ debugLog2("session agent_end", {
1122
+ agent: agent.name,
1123
+ output
1124
+ });
1125
+ });
1126
+ session.on("agent_handoff", (_context, fromAgent, toAgent) => {
1127
+ debugLog2("session agent_handoff", {
1128
+ from: fromAgent.name,
1129
+ to: toAgent.name
1130
+ });
1131
+ });
1132
+ session.on("agent_tool_start", (_context, agent, tool2, details) => {
1133
+ debugLog2("session agent_tool_start", {
1134
+ agent: agent.name,
1135
+ tool: tool2.name,
1136
+ toolCall: details.toolCall
1137
+ });
1138
+ });
1139
+ session.on("agent_tool_end", (_context, agent, tool2, result, details) => {
1140
+ debugLog2("session agent_tool_end", {
1141
+ agent: agent.name,
1142
+ tool: tool2.name,
1143
+ result,
1144
+ toolCall: details.toolCall
1145
+ });
1146
+ });
1147
+ session.on("history_added", (item) => {
1148
+ debugLog2("session history_added", item);
1149
+ });
1150
+ session.on("error", (sessionError) => {
1151
+ debugLog2("session error", sessionError);
1152
+ });
793
1153
  session.on("audio_start", handleSessionAudioStart);
794
1154
  session.on("audio_stopped", handleSessionAudioStopped);
795
1155
  session.on("audio_interrupted", handleSessionAudioInterrupted);
@@ -828,6 +1188,17 @@ function useWebVoiceAgent(options) {
828
1188
  setAgentVoiceStateIfChanged("idle");
829
1189
  try {
830
1190
  const runtimeConfig = await runtimeConfigPromise;
1191
+ debugLog2("resolved runtime config", {
1192
+ routes: runtimeConfig.routes.map((route) => route.path),
1193
+ functionModules: Object.keys(runtimeConfig.functionModuleLoaders),
1194
+ agents: runtimeConfig.agents.map((agent2) => ({
1195
+ key: agent2.key,
1196
+ name: agent2.name,
1197
+ isPrimary: agent2.isPrimary,
1198
+ functionModules: Object.keys(agent2.functionModuleLoaders)
1199
+ })),
1200
+ warnings: runtimeConfig.warnings
1201
+ });
831
1202
  const requestPayload = runtimeConfig.modelOverride ? { model: runtimeConfig.modelOverride } : {};
832
1203
  const secretPayload = await backendClient.createClientSecret(requestPayload);
833
1204
  const backendFunctionsResult = await backendClient.listFunctions();
@@ -835,6 +1206,8 @@ function useWebVoiceAgent(options) {
835
1206
  navigate: options.navigate,
836
1207
  routes: runtimeConfig.routes,
837
1208
  functionModuleLoaders: runtimeConfig.functionModuleLoaders,
1209
+ agents: runtimeConfig.agents,
1210
+ primaryAgentKey: runtimeConfig.primaryAgentKey,
838
1211
  backendFunctions: backendFunctionsResult.functions,
839
1212
  executeBackendFunction: backendClient.executeFunction
840
1213
  });
@@ -850,6 +1223,7 @@ function useWebVoiceAgent(options) {
850
1223
  setStatus("connected");
851
1224
  } catch (startError) {
852
1225
  const message = formatError(startError);
1226
+ debugLog2("session start failed", { message, error: startError });
853
1227
  setError(message);
854
1228
  setStatus("error");
855
1229
  setAgentVoiceStateIfChanged("idle");
@@ -880,12 +1254,361 @@ function useWebVoiceAgent(options) {
880
1254
  stop
881
1255
  };
882
1256
  }
1257
+
1258
+ // src/orb/NavaiHeroOrb.tsx
1259
+ import { useEffect as useEffect3, useMemo as useMemo2, useState as useState3 } from "react";
1260
+
1261
+ // src/orb/dynamic.tsx
1262
+ import { lazy, Suspense, useEffect as useEffect2, useState as useState2 } from "react";
1263
+ import { jsx } from "react/jsx-runtime";
1264
+ function dynamic(loader, options = {}) {
1265
+ const { ssr = true, loading: LoadingComponent } = options;
1266
+ const LazyComponent = lazy(async () => {
1267
+ const loaded = await loader();
1268
+ if (typeof loaded === "function") {
1269
+ return { default: loaded };
1270
+ }
1271
+ return loaded;
1272
+ });
1273
+ function DynamicComponent(props) {
1274
+ const [isClientReady, setIsClientReady] = useState2(ssr);
1275
+ useEffect2(() => {
1276
+ if (!ssr) {
1277
+ setIsClientReady(true);
1278
+ }
1279
+ }, [ssr]);
1280
+ if (!isClientReady) {
1281
+ return null;
1282
+ }
1283
+ const fallback = LoadingComponent ? /* @__PURE__ */ jsx(LoadingComponent, {}) : null;
1284
+ return /* @__PURE__ */ jsx(Suspense, { fallback, children: /* @__PURE__ */ jsx(LazyComponent, { ...props }) });
1285
+ }
1286
+ DynamicComponent.displayName = "DynamicComponent";
1287
+ return DynamicComponent;
1288
+ }
1289
+
1290
+ // src/orb/NavaiHeroOrb.tsx
1291
+ import { jsx as jsx2 } from "react/jsx-runtime";
1292
+ var Orb2 = dynamic(() => import("./Orb-B4OSC3XR.js"), {
1293
+ ssr: false
1294
+ });
1295
+ var ORB_DELAY_MS_MIN = 0;
1296
+ var ORB_DELAY_MS_MAX = 6e4;
1297
+ var DEFAULT_AUTOPLAY_DELAY_MS = 9e3;
1298
+ var DEFAULT_REVEAL_DELAY_MS = 5200;
1299
+ function clampNavaiOrbDelayMs(value, fallback) {
1300
+ const numericValue = Number.isFinite(value) ? value : fallback;
1301
+ return Math.min(ORB_DELAY_MS_MAX, Math.max(ORB_DELAY_MS_MIN, numericValue));
1302
+ }
1303
+ function NavaiHeroOrb({
1304
+ className = "",
1305
+ backgroundColor = "#000000",
1306
+ isAgentSpeaking = false,
1307
+ hoverIntensitySpeaking = 0.66,
1308
+ hoverIntensityIdle = 0.08,
1309
+ revealDelayMs = DEFAULT_REVEAL_DELAY_MS,
1310
+ autoplayDelayMs = DEFAULT_AUTOPLAY_DELAY_MS
1311
+ }) {
1312
+ useNavaiVoiceOrbStyles();
1313
+ const resolvedRevealDelayMs = clampNavaiOrbDelayMs(revealDelayMs, DEFAULT_REVEAL_DELAY_MS);
1314
+ const resolvedAutoplayDelayMs = clampNavaiOrbDelayMs(autoplayDelayMs, DEFAULT_AUTOPLAY_DELAY_MS);
1315
+ const [isOrbReady, setIsOrbReady] = useState3(resolvedRevealDelayMs === 0);
1316
+ const [isOrbAutoAnimating, setIsOrbAutoAnimating] = useState3(resolvedAutoplayDelayMs === 0);
1317
+ useEffect3(() => {
1318
+ if (typeof window === "undefined" || resolvedRevealDelayMs === 0) {
1319
+ return;
1320
+ }
1321
+ const revealOrb = () => setIsOrbReady(true);
1322
+ window.addEventListener("pointerdown", revealOrb, { passive: true, once: true });
1323
+ window.addEventListener("touchstart", revealOrb, { passive: true, once: true });
1324
+ window.addEventListener("keydown", revealOrb, { once: true });
1325
+ const timeoutId = window.setTimeout(revealOrb, resolvedRevealDelayMs);
1326
+ return () => {
1327
+ window.removeEventListener("pointerdown", revealOrb);
1328
+ window.removeEventListener("touchstart", revealOrb);
1329
+ window.removeEventListener("keydown", revealOrb);
1330
+ window.clearTimeout(timeoutId);
1331
+ };
1332
+ }, [resolvedRevealDelayMs]);
1333
+ useEffect3(() => {
1334
+ if (typeof window === "undefined" || resolvedAutoplayDelayMs === 0) {
1335
+ return;
1336
+ }
1337
+ let started = false;
1338
+ const startOrbAnimation = () => {
1339
+ if (started) {
1340
+ return;
1341
+ }
1342
+ started = true;
1343
+ setIsOrbAutoAnimating(true);
1344
+ };
1345
+ if (navigator.userActivation?.hasBeenActive) {
1346
+ startOrbAnimation();
1347
+ }
1348
+ window.addEventListener("pointerdown", startOrbAnimation, { passive: true, once: true });
1349
+ window.addEventListener("touchstart", startOrbAnimation, { passive: true, once: true });
1350
+ window.addEventListener("keydown", startOrbAnimation, { once: true });
1351
+ const timeoutId = window.setTimeout(startOrbAnimation, resolvedAutoplayDelayMs);
1352
+ return () => {
1353
+ window.removeEventListener("pointerdown", startOrbAnimation);
1354
+ window.removeEventListener("touchstart", startOrbAnimation);
1355
+ window.removeEventListener("keydown", startOrbAnimation);
1356
+ window.clearTimeout(timeoutId);
1357
+ };
1358
+ }, [resolvedAutoplayDelayMs]);
1359
+ const orbHoverIntensity = useMemo2(() => {
1360
+ return isAgentSpeaking ? hoverIntensitySpeaking : hoverIntensityIdle;
1361
+ }, [hoverIntensityIdle, hoverIntensitySpeaking, isAgentSpeaking]);
1362
+ if (!isOrbReady) {
1363
+ return null;
1364
+ }
1365
+ return /* @__PURE__ */ jsx2("div", { className: ["navai-voice-orb-hero", className].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx2(
1366
+ Orb2,
1367
+ {
1368
+ hoverIntensity: orbHoverIntensity,
1369
+ rotateOnHover: true,
1370
+ forceHoverState: isAgentSpeaking,
1371
+ enablePointerHover: false,
1372
+ animate: isAgentSpeaking || isOrbAutoAnimating,
1373
+ backgroundColor
1374
+ }
1375
+ ) });
1376
+ }
1377
+
1378
+ // src/orb/NavaiMiniOrbDock.tsx
1379
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
1380
+ var Orb3 = dynamic(() => import("./Orb-B4OSC3XR.js"), {
1381
+ ssr: false
1382
+ });
1383
+ function NavaiMiniOrbDock({
1384
+ className = "",
1385
+ style,
1386
+ themeMode = "dark",
1387
+ placement = "bottom-right",
1388
+ isActive = false,
1389
+ isConnected = false,
1390
+ isDisabled = false,
1391
+ isAgentSpeaking = false,
1392
+ animateOrb = true,
1393
+ backgroundColor = "#060914",
1394
+ buttonAriaLabel,
1395
+ buttonIcon,
1396
+ buttonType = "button",
1397
+ onButtonClick,
1398
+ statusMessage = "",
1399
+ isError = false,
1400
+ ariaMessage = ""
1401
+ }) {
1402
+ useNavaiVoiceOrbStyles();
1403
+ const dockClassName = ["navai-voice-orb-dock", `is-${placement}`, themeMode === "light" ? "is-light" : "", className].filter(Boolean).join(" ");
1404
+ const shouldHighlightOrb = isAgentSpeaking || isActive;
1405
+ const orbHoverIntensity = isAgentSpeaking ? 0.66 : 0.08;
1406
+ return /* @__PURE__ */ jsxs("aside", { className: dockClassName, style, children: [
1407
+ /* @__PURE__ */ jsxs("div", { className: "navai-voice-orb-wrap", children: [
1408
+ /* @__PURE__ */ jsx3("div", { className: ["navai-voice-orb-surface", shouldHighlightOrb ? "is-highlighted" : ""].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx3(
1409
+ Orb3,
1410
+ {
1411
+ hoverIntensity: orbHoverIntensity,
1412
+ rotateOnHover: true,
1413
+ forceHoverState: isAgentSpeaking,
1414
+ enablePointerHover: false,
1415
+ animate: animateOrb,
1416
+ backgroundColor
1417
+ }
1418
+ ) }),
1419
+ /* @__PURE__ */ jsx3("div", { className: ["navai-voice-orb-button-shell", isConnected ? "is-active" : ""].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx3(
1420
+ "button",
1421
+ {
1422
+ type: buttonType,
1423
+ className: [
1424
+ "navai-voice-orb-button",
1425
+ isConnected ? "is-active" : "",
1426
+ isActive && !isConnected ? "is-connecting" : ""
1427
+ ].filter(Boolean).join(" "),
1428
+ onClick: onButtonClick,
1429
+ disabled: isDisabled,
1430
+ "aria-label": buttonAriaLabel,
1431
+ children: buttonIcon
1432
+ }
1433
+ ) })
1434
+ ] }),
1435
+ statusMessage ? /* @__PURE__ */ jsx3("p", { className: ["navai-voice-orb-status", isError ? "is-error" : ""].filter(Boolean).join(" "), role: "status", children: statusMessage }) : null,
1436
+ /* @__PURE__ */ jsx3("span", { className: "navai-voice-orb-live", "aria-live": "polite", children: ariaMessage })
1437
+ ] });
1438
+ }
1439
+
1440
+ // src/orb/NavaiVoiceHeroOrb.tsx
1441
+ import { useEffect as useEffect4 } from "react";
1442
+
1443
+ // src/orb/NavaiVoiceOrbDock.tsx
1444
+ import { useCallback as useCallback2, useMemo as useMemo3 } from "react";
1445
+
1446
+ // src/orb/NavaiVoiceOrbDockMicIcon.tsx
1447
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1448
+ function NavaiVoiceOrbDockMicIcon({
1449
+ isActive = false,
1450
+ size = 20
1451
+ }) {
1452
+ return /* @__PURE__ */ jsxs2(
1453
+ "svg",
1454
+ {
1455
+ width: size,
1456
+ height: size,
1457
+ viewBox: "0 0 24 24",
1458
+ fill: "none",
1459
+ stroke: "currentColor",
1460
+ strokeWidth: "2",
1461
+ strokeLinecap: "round",
1462
+ strokeLinejoin: "round",
1463
+ "aria-hidden": "true",
1464
+ className: ["navai-voice-orb-icon", isActive ? "is-pulsing" : ""].filter(Boolean).join(" "),
1465
+ children: [
1466
+ /* @__PURE__ */ jsx4("path", { d: "M12 3a3 3 0 0 0-3 3v6a3 3 0 1 0 6 0V6a3 3 0 0 0-3-3Z" }),
1467
+ /* @__PURE__ */ jsx4("path", { d: "M19 10v2a7 7 0 1 1-14 0v-2" }),
1468
+ /* @__PURE__ */ jsx4("path", { d: "M12 19v3" })
1469
+ ]
1470
+ }
1471
+ );
1472
+ }
1473
+
1474
+ // src/orb/NavaiVoiceOrbDockSpinnerIcon.tsx
1475
+ import { jsx as jsx5 } from "react/jsx-runtime";
1476
+ function NavaiVoiceOrbDockSpinnerIcon({
1477
+ size = 20
1478
+ }) {
1479
+ const style = {
1480
+ width: size,
1481
+ height: size
1482
+ };
1483
+ return /* @__PURE__ */ jsx5("span", { "aria-hidden": "true", className: "navai-voice-orb-spinner", style });
1484
+ }
1485
+
1486
+ // src/orb/NavaiVoiceOrbDock.tsx
1487
+ import { jsx as jsx6 } from "react/jsx-runtime";
1488
+ var DEFAULT_MESSAGES = {
1489
+ ariaStart: "Activate NAVAI voice",
1490
+ ariaStop: "Deactivate NAVAI voice",
1491
+ idle: "NAVAI ready to start.",
1492
+ connecting: "Connecting NAVAI voice...",
1493
+ listening: "NAVAI is listening.",
1494
+ speaking: "NAVAI is speaking.",
1495
+ errorPrefix: "NAVAI error"
1496
+ };
1497
+ function resolveNavaiVoiceOrbRuntimeSnapshot(agent) {
1498
+ return {
1499
+ status: agent.status,
1500
+ agentVoiceState: agent.agentVoiceState,
1501
+ isAgentSpeaking: agent.isAgentSpeaking,
1502
+ error: agent.error
1503
+ };
1504
+ }
1505
+ function resolveStatusMessage(runtimeSnapshot, messages) {
1506
+ if (runtimeSnapshot.error) {
1507
+ return `${messages.errorPrefix}: ${runtimeSnapshot.error}`;
1508
+ }
1509
+ if (runtimeSnapshot.isAgentSpeaking) {
1510
+ return messages.speaking;
1511
+ }
1512
+ if (runtimeSnapshot.status === "connecting") {
1513
+ return messages.connecting;
1514
+ }
1515
+ if (runtimeSnapshot.status === "connected") {
1516
+ return messages.listening;
1517
+ }
1518
+ return messages.idle;
1519
+ }
1520
+ function NavaiVoiceOrbDock({
1521
+ agent,
1522
+ className,
1523
+ style,
1524
+ themeMode = "dark",
1525
+ placement = "bottom-right",
1526
+ backgroundColorLight = "#f4f6fb",
1527
+ backgroundColorDark = "#060914",
1528
+ showStatus = true,
1529
+ messages
1530
+ }) {
1531
+ const resolvedMessages = useMemo3(() => ({ ...DEFAULT_MESSAGES, ...messages }), [messages]);
1532
+ const runtimeSnapshot = useMemo3(() => resolveNavaiVoiceOrbRuntimeSnapshot(agent), [agent]);
1533
+ const statusMessage = showStatus ? resolveStatusMessage(runtimeSnapshot, resolvedMessages) : "";
1534
+ const isError = runtimeSnapshot.status === "error" || Boolean(runtimeSnapshot.error);
1535
+ const isConnecting = runtimeSnapshot.status === "connecting";
1536
+ const isActive = runtimeSnapshot.status === "connecting" || runtimeSnapshot.status === "connected";
1537
+ const isDisabled = agent.isConnecting;
1538
+ const shouldAnimateOrb = runtimeSnapshot.status !== "error";
1539
+ const handleToggle = useCallback2(() => {
1540
+ if (agent.isConnecting) {
1541
+ return;
1542
+ }
1543
+ if (agent.isConnected) {
1544
+ agent.stop();
1545
+ return;
1546
+ }
1547
+ void agent.start();
1548
+ }, [agent]);
1549
+ return /* @__PURE__ */ jsx6(
1550
+ NavaiMiniOrbDock,
1551
+ {
1552
+ className,
1553
+ style,
1554
+ themeMode,
1555
+ placement,
1556
+ isActive,
1557
+ isConnected: agent.isConnected,
1558
+ isDisabled,
1559
+ isAgentSpeaking: agent.isAgentSpeaking,
1560
+ animateOrb: shouldAnimateOrb,
1561
+ backgroundColor: themeMode === "light" ? backgroundColorLight : backgroundColorDark,
1562
+ buttonAriaLabel: isConnecting ? resolvedMessages.connecting : agent.isConnected ? resolvedMessages.ariaStop : resolvedMessages.ariaStart,
1563
+ buttonIcon: isConnecting ? /* @__PURE__ */ jsx6(NavaiVoiceOrbDockSpinnerIcon, {}) : /* @__PURE__ */ jsx6(NavaiVoiceOrbDockMicIcon, { isActive: agent.isConnected || agent.isAgentSpeaking }),
1564
+ onButtonClick: handleToggle,
1565
+ statusMessage,
1566
+ isError,
1567
+ ariaMessage: statusMessage
1568
+ }
1569
+ );
1570
+ }
1571
+
1572
+ // src/orb/NavaiVoiceHeroOrb.tsx
1573
+ import { jsx as jsx7 } from "react/jsx-runtime";
1574
+ function NavaiVoiceHeroOrb({
1575
+ agent,
1576
+ themeMode = "dark",
1577
+ backgroundColorLight = "#ffffff",
1578
+ backgroundColorDark = "#000000",
1579
+ onRuntimeSnapshotChange,
1580
+ ...orbProps
1581
+ }) {
1582
+ const runtimeSnapshot = resolveNavaiVoiceOrbRuntimeSnapshot(agent);
1583
+ useEffect4(() => {
1584
+ if (typeof onRuntimeSnapshotChange === "function") {
1585
+ onRuntimeSnapshotChange(runtimeSnapshot);
1586
+ }
1587
+ }, [onRuntimeSnapshotChange, runtimeSnapshot]);
1588
+ return /* @__PURE__ */ jsx7(
1589
+ NavaiHeroOrb,
1590
+ {
1591
+ ...orbProps,
1592
+ isAgentSpeaking: runtimeSnapshot.isAgentSpeaking,
1593
+ backgroundColor: themeMode === "light" ? backgroundColorLight : backgroundColorDark,
1594
+ className: themeMode === "light" ? "is-light" : ""
1595
+ }
1596
+ );
1597
+ }
883
1598
  export {
1599
+ NavaiHeroOrb,
1600
+ NavaiMiniOrbDock,
1601
+ NavaiVoiceHeroOrb,
1602
+ NavaiVoiceOrbDock,
1603
+ NavaiVoiceOrbDockMicIcon,
1604
+ Orb,
884
1605
  buildNavaiAgent,
1606
+ clampNavaiOrbDelayMs,
885
1607
  createNavaiBackendClient,
886
1608
  getNavaiRoutePromptLines,
887
1609
  loadNavaiFunctions,
888
1610
  resolveNavaiFrontendRuntimeConfig,
889
1611
  resolveNavaiRoute,
1612
+ resolveNavaiVoiceOrbRuntimeSnapshot,
890
1613
  useWebVoiceAgent
891
1614
  };