@navai/voice-frontend 0.1.1 → 0.1.2

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.en.md ADDED
@@ -0,0 +1,320 @@
1
+ # @navai/voice-frontend
2
+
3
+ <p>
4
+ <a href="./README.es.md"><img alt="Idioma Espanol" src="https://img.shields.io/badge/Idioma-ES-0A66C2?style=for-the-badge"></a>
5
+ <a href="./README.en.md"><img alt="Language English" src="https://img.shields.io/badge/Language-EN-1D9A6C?style=for-the-badge"></a>
6
+ </p>
7
+
8
+ Frontend package to build Navai voice agents in web applications.
9
+
10
+ It removes repeated boilerplate for:
11
+
12
+ 1. Realtime client secret requests.
13
+ 2. Route-aware navigation tools.
14
+ 3. Dynamic local function loading.
15
+ 4. Optional backend function bridging.
16
+ 5. React hook lifecycle for connect/disconnect.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @navai/voice-frontend @openai/agents zod
22
+ npm install react
23
+ ```
24
+
25
+ ## Package Architecture
26
+
27
+ This package is intentionally split by concern:
28
+
29
+ 1. `src/backend.ts`
30
+ HTTP client for backend routes:
31
+ - `POST /navai/realtime/client-secret`
32
+ - `GET /navai/functions`
33
+ - `POST /navai/functions/execute`
34
+
35
+ 2. `src/runtime.ts`
36
+ Runtime resolver for:
37
+ - route module selection
38
+ - function module filtering by `NAVAI_FUNCTIONS_FOLDERS`
39
+ - optional model override
40
+
41
+ 3. `src/functions.ts`
42
+ Local function loader:
43
+ - imports modules from generated loaders
44
+ - converts exports into normalized callable tool definitions
45
+
46
+ 4. `src/agent.ts`
47
+ Agent builder:
48
+ - creates `RealtimeAgent`
49
+ - injects built-in tools (`navigate_to`, `execute_app_function`)
50
+ - optionally adds direct alias tools for each allowed function
51
+
52
+ 5. `src/useWebVoiceAgent.ts`
53
+ React lifecycle wrapper:
54
+ - builds runtime config
55
+ - requests client secret
56
+ - discovers backend functions
57
+ - builds agent
58
+ - opens/closes `RealtimeSession`
59
+
60
+ 6. `src/routes.ts`
61
+ Route matching helpers for natural language to path resolution.
62
+
63
+ ## End-to-End Runtime Flow
64
+
65
+ Hook-driven runtime flow (`useWebVoiceAgent`):
66
+
67
+ 1. Resolve runtime config from `moduleLoaders` + `defaultRoutes` + env/options.
68
+ 2. Create backend client with `apiBaseUrl` or `NAVAI_API_URL`.
69
+ 3. On `start()`:
70
+ - request client secret.
71
+ - fetch backend function list.
72
+ - build Navai agent with local + backend functions.
73
+ - connect `RealtimeSession`.
74
+ 4. On `stop()`:
75
+ - close session and reset state.
76
+
77
+ State machine exposed by hook:
78
+
79
+ - `idle`
80
+ - `connecting`
81
+ - `connected`
82
+ - `error`
83
+
84
+ Agent voice state exposed by the hook:
85
+
86
+ - `agentVoiceState`: `idle | speaking`
87
+ - `isAgentSpeaking`: `boolean`
88
+
89
+ `agentVoiceState` is driven by realtime events `audio_start`, `audio_stopped`, and `audio_interrupted`.
90
+
91
+ ## Public API
92
+
93
+ Main exports:
94
+
95
+ - `buildNavaiAgent(...)`
96
+ - `createNavaiBackendClient(...)`
97
+ - `resolveNavaiFrontendRuntimeConfig(...)`
98
+ - `loadNavaiFunctions(...)`
99
+ - `useWebVoiceAgent(...)`
100
+ - `resolveNavaiRoute(...)`
101
+ - `getNavaiRoutePromptLines(...)`
102
+
103
+ Useful types:
104
+
105
+ - `NavaiRoute`
106
+ - `NavaiFunctionDefinition`
107
+ - `NavaiFunctionsRegistry`
108
+ - `NavaiBackendFunctionDefinition`
109
+ - `UseWebVoiceAgentOptions`
110
+
111
+ ## Tool Model and Behavior
112
+
113
+ `buildNavaiAgent` always registers:
114
+
115
+ - `navigate_to`
116
+ - `execute_app_function`
117
+
118
+ Optional direct alias tools:
119
+
120
+ - for each allowed function name, a direct tool can be created.
121
+ - reserved names are never used as direct tools (`navigate_to`, `execute_app_function`).
122
+ - invalid tool ids are skipped (kept accessible via `execute_app_function`).
123
+
124
+ Execution precedence:
125
+
126
+ 1. Try frontend/local function first.
127
+ 2. If missing, try backend function.
128
+ 3. If both exist with same name, frontend wins and backend is ignored with warning.
129
+
130
+ ## Dynamic Function Loading Internals
131
+
132
+ `loadNavaiFunctions` supports module export shapes:
133
+
134
+ 1. Exported function.
135
+ 2. Exported class (instance methods become functions).
136
+ 3. Exported object (callable members become functions).
137
+
138
+ Name normalization rules:
139
+
140
+ - snake_case lowercase.
141
+ - invalid chars removed.
142
+ - collisions are renamed with suffixes (`_2`, `_3`, ...).
143
+
144
+ Argument mapping rules:
145
+
146
+ - `payload.args` or `payload.arguments` as direct args.
147
+ - else `payload.value` as first arg.
148
+ - else full payload as first arg.
149
+ - context appended when arity indicates one more argument.
150
+
151
+ For class methods:
152
+
153
+ - constructor args: `payload.constructorArgs`.
154
+ - method args: `payload.methodArgs`.
155
+
156
+ ## Runtime Resolution and Env Precedence
157
+
158
+ `resolveNavaiFrontendRuntimeConfig` input priority:
159
+
160
+ 1. Explicit function args.
161
+ 2. Env object keys.
162
+ 3. Package defaults.
163
+
164
+ Keys used:
165
+
166
+ - `NAVAI_ROUTES_FILE`
167
+ - `NAVAI_FUNCTIONS_FOLDERS`
168
+ - `NAVAI_REALTIME_MODEL`
169
+
170
+ Defaults:
171
+
172
+ - routes file: `src/ai/routes.ts`
173
+ - functions folder: `src/ai/functions-modules`
174
+
175
+ Path matcher formats:
176
+
177
+ - folder: `src/ai/functions-modules`
178
+ - recursive: `src/ai/functions-modules/...`
179
+ - wildcard: `src/features/*/voice-functions`
180
+ - explicit file: `src/ai/functions-modules/secret.ts`
181
+ - CSV list: `a,b,c`
182
+
183
+ Fallback behavior:
184
+
185
+ - if configured folders match no modules, warning is emitted.
186
+ - resolver falls back to default functions folder.
187
+
188
+ ## Backend Client Behavior
189
+
190
+ `createNavaiBackendClient` base URL priority:
191
+
192
+ 1. `apiBaseUrl` option.
193
+ 2. `env.NAVAI_API_URL`.
194
+ 3. fallback `http://localhost:3000`.
195
+
196
+ Methods:
197
+
198
+ - `createClientSecret(input?)`
199
+ - `listFunctions()`
200
+ - `executeFunction({ functionName, payload })`
201
+
202
+ Error handling:
203
+
204
+ - network/HTTP failures throw for create/execute.
205
+ - function listing returns warnings and empty list on failures.
206
+
207
+ ## Generated Module Loader CLI
208
+
209
+ This package ships:
210
+
211
+ - `navai-generate-web-loaders`
212
+
213
+ Default command behavior:
214
+
215
+ 1. Reads `.env` and process env.
216
+ 2. Resolves `NAVAI_FUNCTIONS_FOLDERS` and `NAVAI_ROUTES_FILE`.
217
+ 3. Selects modules only from configured function paths.
218
+ 4. Optionally includes configured route module if it differs from default route module.
219
+ 5. Writes `src/ai/generated-module-loaders.ts`.
220
+
221
+ Manual usage:
222
+
223
+ ```bash
224
+ navai-generate-web-loaders
225
+ ```
226
+
227
+ Useful flags:
228
+
229
+ - `--project-root <path>`
230
+ - `--src-root <path>`
231
+ - `--output-file <path>`
232
+ - `--env-file <path>`
233
+ - `--default-functions-folder <path>`
234
+ - `--default-routes-file <path>`
235
+ - `--type-import <module>`
236
+ - `--export-name <identifier>`
237
+
238
+ ## Auto Setup on npm Install
239
+
240
+ Postinstall script can auto-add missing scripts in consumer app:
241
+
242
+ - `generate:module-loaders` -> `navai-generate-web-loaders`
243
+ - `predev` -> `npm run generate:module-loaders`
244
+ - `prebuild` -> `npm run generate:module-loaders`
245
+ - `pretypecheck` -> `npm run generate:module-loaders`
246
+ - `prelint` -> `npm run generate:module-loaders`
247
+
248
+ Rules:
249
+
250
+ - only missing scripts are added.
251
+ - existing scripts are never overwritten.
252
+
253
+ Disable auto setup:
254
+
255
+ - `NAVAI_SKIP_AUTO_SETUP=1`
256
+ - or `NAVAI_SKIP_FRONTEND_AUTO_SETUP=1`
257
+
258
+ Manual setup runner:
259
+
260
+ ```bash
261
+ npx navai-setup-voice-frontend
262
+ ```
263
+
264
+ ## Integration Examples
265
+
266
+ Imperative integration:
267
+
268
+ ```ts
269
+ import { RealtimeSession } from "@openai/agents/realtime";
270
+ import { buildNavaiAgent, createNavaiBackendClient } from "@navai/voice-frontend";
271
+ import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
272
+ import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
273
+
274
+ const backend = createNavaiBackendClient({ apiBaseUrl: "http://localhost:3000" });
275
+ const secret = await backend.createClientSecret();
276
+ const backendList = await backend.listFunctions();
277
+
278
+ const { agent, warnings } = await buildNavaiAgent({
279
+ navigate: (path) => router.navigate(path),
280
+ routes: NAVAI_ROUTE_ITEMS,
281
+ functionModuleLoaders: NAVAI_WEB_MODULE_LOADERS,
282
+ backendFunctions: backendList.functions,
283
+ executeBackendFunction: backend.executeFunction
284
+ });
285
+
286
+ warnings.forEach((w) => console.warn(w));
287
+
288
+ const session = new RealtimeSession(agent);
289
+ await session.connect({ apiKey: secret.value });
290
+ ```
291
+
292
+ React hook integration:
293
+
294
+ ```ts
295
+ import { useWebVoiceAgent } from "@navai/voice-frontend";
296
+ import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
297
+ import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
298
+
299
+ const voice = useWebVoiceAgent({
300
+ navigate: (path) => router.navigate(path),
301
+ moduleLoaders: NAVAI_WEB_MODULE_LOADERS,
302
+ defaultRoutes: NAVAI_ROUTE_ITEMS,
303
+ env: import.meta.env as Record<string, string | undefined>
304
+ });
305
+ ```
306
+
307
+ ## Operational Notes
308
+
309
+ - warnings are emitted with `console.warn` from runtime, backend list, and agent builder.
310
+ - unknown function execution returns structured `ok: false` payload.
311
+ - if route module fails to load or has invalid shape, resolver falls back to default routes.
312
+
313
+ ## Related Docs
314
+
315
+ - Spanish version: `README.es.md`
316
+ - English version: `README.en.md`
317
+ - Backend package: `../voice-backend/README.md`
318
+ - Mobile package: `../voice-mobile/README.md`
319
+ - Playground Web: `../../apps/playground-web/README.md`
320
+ - Playground API: `../../apps/playground-api/README.md`
package/README.es.md ADDED
@@ -0,0 +1,320 @@
1
+ # @navai/voice-frontend
2
+
3
+ <p>
4
+ <a href="./README.es.md"><img alt="Idioma Espanol" src="https://img.shields.io/badge/Idioma-ES-0A66C2?style=for-the-badge"></a>
5
+ <a href="./README.en.md"><img alt="Language English" src="https://img.shields.io/badge/Language-EN-1D9A6C?style=for-the-badge"></a>
6
+ </p>
7
+
8
+ Paquete frontend para construir agentes de voz Navai en aplicaciones web.
9
+
10
+ El objetivo es evitar boilerplate repetido para:
11
+
12
+ 1. Solicitud de `client_secret` realtime.
13
+ 2. Tools de navegacion basadas en rutas permitidas.
14
+ 3. Carga dinamica de funciones locales.
15
+ 4. Puente opcional con funciones backend.
16
+ 5. Ciclo de vida React para conectar/desconectar sesion.
17
+
18
+ ## Instalacion
19
+
20
+ ```bash
21
+ npm install @navai/voice-frontend @openai/agents zod
22
+ npm install react
23
+ ```
24
+
25
+ ## Arquitectura del Paquete
26
+
27
+ El paquete esta separado por responsabilidades:
28
+
29
+ 1. `src/backend.ts`
30
+ Cliente HTTP para rutas backend:
31
+ - `POST /navai/realtime/client-secret`
32
+ - `GET /navai/functions`
33
+ - `POST /navai/functions/execute`
34
+
35
+ 2. `src/runtime.ts`
36
+ Resolver de runtime para:
37
+ - seleccion de modulo de rutas
38
+ - filtrado de modulos de funciones por `NAVAI_FUNCTIONS_FOLDERS`
39
+ - override opcional de modelo
40
+
41
+ 3. `src/functions.ts`
42
+ Loader de funciones locales:
43
+ - importa modulos desde loaders generados
44
+ - transforma exports en definiciones de tools normalizadas y ejecutables
45
+
46
+ 4. `src/agent.ts`
47
+ Builder del agente:
48
+ - crea `RealtimeAgent`
49
+ - inyecta tools base (`navigate_to`, `execute_app_function`)
50
+ - agrega aliases directos por funcion permitida cuando aplica
51
+
52
+ 5. `src/useWebVoiceAgent.ts`
53
+ Wrapper de ciclo de vida React:
54
+ - construye runtime config
55
+ - solicita client secret
56
+ - descubre funciones backend
57
+ - construye el agente
58
+ - abre/cierra `RealtimeSession`
59
+
60
+ 6. `src/routes.ts`
61
+ Helpers para resolver texto natural hacia rutas permitidas.
62
+
63
+ ## Flujo Runtime de Punta a Punta
64
+
65
+ Flujo del hook (`useWebVoiceAgent`):
66
+
67
+ 1. Resuelve runtime config desde `moduleLoaders` + `defaultRoutes` + env/opciones.
68
+ 2. Crea backend client con `apiBaseUrl` o `NAVAI_API_URL`.
69
+ 3. En `start()`:
70
+ - solicita client secret.
71
+ - solicita listado de funciones backend.
72
+ - construye agente Navai con funciones locales + backend.
73
+ - conecta `RealtimeSession`.
74
+ 4. En `stop()`:
75
+ - cierra sesion y resetea estado.
76
+
77
+ Maquina de estados expuesta por el hook:
78
+
79
+ - `idle`
80
+ - `connecting`
81
+ - `connected`
82
+ - `error`
83
+
84
+ Estado de voz del agente expuesto por el hook:
85
+
86
+ - `agentVoiceState`: `idle | speaking`
87
+ - `isAgentSpeaking`: `boolean`
88
+
89
+ `agentVoiceState` se actualiza con los eventos realtime `audio_start`, `audio_stopped` y `audio_interrupted`.
90
+
91
+ ## API Publica
92
+
93
+ Exports principales:
94
+
95
+ - `buildNavaiAgent(...)`
96
+ - `createNavaiBackendClient(...)`
97
+ - `resolveNavaiFrontendRuntimeConfig(...)`
98
+ - `loadNavaiFunctions(...)`
99
+ - `useWebVoiceAgent(...)`
100
+ - `resolveNavaiRoute(...)`
101
+ - `getNavaiRoutePromptLines(...)`
102
+
103
+ Tipos utiles:
104
+
105
+ - `NavaiRoute`
106
+ - `NavaiFunctionDefinition`
107
+ - `NavaiFunctionsRegistry`
108
+ - `NavaiBackendFunctionDefinition`
109
+ - `UseWebVoiceAgentOptions`
110
+
111
+ ## Modelo de Tools y Comportamiento
112
+
113
+ `buildNavaiAgent` siempre registra:
114
+
115
+ - `navigate_to`
116
+ - `execute_app_function`
117
+
118
+ Aliases directos opcionales:
119
+
120
+ - para cada funcion permitida se puede crear una tool directa.
121
+ - nombres reservados nunca se exponen como tool directa (`navigate_to`, `execute_app_function`).
122
+ - ids invalidos de tool se omiten (la funcion sigue accesible via `execute_app_function`).
123
+
124
+ Precedencia de ejecucion:
125
+
126
+ 1. Intenta funcion local/frontend.
127
+ 2. Si no existe, intenta funcion backend.
128
+ 3. Si ambas tienen el mismo nombre, gana frontend y backend se ignora con warning.
129
+
130
+ ## Internos de Carga Dinamica de Funciones
131
+
132
+ `loadNavaiFunctions` soporta estos formatos de export:
133
+
134
+ 1. Funcion exportada.
135
+ 2. Clase exportada (metodos de instancia se vuelven funciones).
136
+ 3. Objeto exportado (miembros callables se vuelven funciones).
137
+
138
+ Reglas de normalizacion de nombre:
139
+
140
+ - snake_case en minusculas.
141
+ - elimina caracteres invalidos.
142
+ - colisiones se renombran con sufijos (`_2`, `_3`, ...).
143
+
144
+ Reglas de mapeo de argumentos:
145
+
146
+ - usa `payload.args` o `payload.arguments` como argumentos directos.
147
+ - si no, usa `payload.value` como primer argumento.
148
+ - si no, usa payload completo como primer argumento.
149
+ - agrega contexto cuando la aridad indica un argumento adicional.
150
+
151
+ Para metodos de clase:
152
+
153
+ - args de constructor: `payload.constructorArgs`.
154
+ - args del metodo: `payload.methodArgs`.
155
+
156
+ ## Resolucion Runtime y Precedencia de Env
157
+
158
+ Prioridad de entrada en `resolveNavaiFrontendRuntimeConfig`:
159
+
160
+ 1. Argumentos explicitos de funcion.
161
+ 2. Claves del objeto env.
162
+ 3. Defaults del paquete.
163
+
164
+ Claves usadas:
165
+
166
+ - `NAVAI_ROUTES_FILE`
167
+ - `NAVAI_FUNCTIONS_FOLDERS`
168
+ - `NAVAI_REALTIME_MODEL`
169
+
170
+ Defaults:
171
+
172
+ - archivo de rutas: `src/ai/routes.ts`
173
+ - carpeta de funciones: `src/ai/functions-modules`
174
+
175
+ Formatos aceptados en matcher de rutas:
176
+
177
+ - carpeta: `src/ai/functions-modules`
178
+ - recursivo: `src/ai/functions-modules/...`
179
+ - wildcard: `src/features/*/voice-functions`
180
+ - archivo explicito: `src/ai/functions-modules/secret.ts`
181
+ - CSV: `a,b,c`
182
+
183
+ Comportamiento fallback:
184
+
185
+ - si `NAVAI_FUNCTIONS_FOLDERS` no matchea modulos, emite warning.
186
+ - hace fallback a carpeta de funciones por defecto.
187
+
188
+ ## Comportamiento del Backend Client
189
+
190
+ Prioridad de base URL en `createNavaiBackendClient`:
191
+
192
+ 1. Opcion `apiBaseUrl`.
193
+ 2. `env.NAVAI_API_URL`.
194
+ 3. Fallback `http://localhost:3000`.
195
+
196
+ Metodos:
197
+
198
+ - `createClientSecret(input?)`
199
+ - `listFunctions()`
200
+ - `executeFunction({ functionName, payload })`
201
+
202
+ Manejo de errores:
203
+
204
+ - fallos de red/HTTP lanzan error en create/execute.
205
+ - el listado de funciones retorna warnings + lista vacia en fallos.
206
+
207
+ ## CLI Generador de Module Loaders
208
+
209
+ Este paquete incluye:
210
+
211
+ - `navai-generate-web-loaders`
212
+
213
+ Comportamiento por defecto:
214
+
215
+ 1. Lee `.env` y env del proceso.
216
+ 2. Resuelve `NAVAI_FUNCTIONS_FOLDERS` y `NAVAI_ROUTES_FILE`.
217
+ 3. Selecciona modulos solo en rutas de funciones configuradas.
218
+ 4. Incluye modulo de rutas configurado cuando difiere del modulo por defecto.
219
+ 5. Escribe `src/ai/generated-module-loaders.ts`.
220
+
221
+ Uso manual:
222
+
223
+ ```bash
224
+ navai-generate-web-loaders
225
+ ```
226
+
227
+ Flags utiles:
228
+
229
+ - `--project-root <path>`
230
+ - `--src-root <path>`
231
+ - `--output-file <path>`
232
+ - `--env-file <path>`
233
+ - `--default-functions-folder <path>`
234
+ - `--default-routes-file <path>`
235
+ - `--type-import <module>`
236
+ - `--export-name <identifier>`
237
+
238
+ ## Auto Configuracion al Instalar desde npm
239
+
240
+ El `postinstall` puede agregar scripts faltantes en el consumidor:
241
+
242
+ - `generate:module-loaders` -> `navai-generate-web-loaders`
243
+ - `predev` -> `npm run generate:module-loaders`
244
+ - `prebuild` -> `npm run generate:module-loaders`
245
+ - `pretypecheck` -> `npm run generate:module-loaders`
246
+ - `prelint` -> `npm run generate:module-loaders`
247
+
248
+ Reglas:
249
+
250
+ - solo agrega scripts faltantes.
251
+ - nunca sobreescribe scripts existentes.
252
+
253
+ Desactivar auto setup:
254
+
255
+ - `NAVAI_SKIP_AUTO_SETUP=1`
256
+ - o `NAVAI_SKIP_FRONTEND_AUTO_SETUP=1`
257
+
258
+ Ejecutor manual de setup:
259
+
260
+ ```bash
261
+ npx navai-setup-voice-frontend
262
+ ```
263
+
264
+ ## Ejemplos de Integracion
265
+
266
+ Integracion imperativa:
267
+
268
+ ```ts
269
+ import { RealtimeSession } from "@openai/agents/realtime";
270
+ import { buildNavaiAgent, createNavaiBackendClient } from "@navai/voice-frontend";
271
+ import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
272
+ import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
273
+
274
+ const backend = createNavaiBackendClient({ apiBaseUrl: "http://localhost:3000" });
275
+ const secret = await backend.createClientSecret();
276
+ const backendList = await backend.listFunctions();
277
+
278
+ const { agent, warnings } = await buildNavaiAgent({
279
+ navigate: (path) => router.navigate(path),
280
+ routes: NAVAI_ROUTE_ITEMS,
281
+ functionModuleLoaders: NAVAI_WEB_MODULE_LOADERS,
282
+ backendFunctions: backendList.functions,
283
+ executeBackendFunction: backend.executeFunction
284
+ });
285
+
286
+ warnings.forEach((w) => console.warn(w));
287
+
288
+ const session = new RealtimeSession(agent);
289
+ await session.connect({ apiKey: secret.value });
290
+ ```
291
+
292
+ Integracion con hook React:
293
+
294
+ ```ts
295
+ import { useWebVoiceAgent } from "@navai/voice-frontend";
296
+ import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
297
+ import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
298
+
299
+ const voice = useWebVoiceAgent({
300
+ navigate: (path) => router.navigate(path),
301
+ moduleLoaders: NAVAI_WEB_MODULE_LOADERS,
302
+ defaultRoutes: NAVAI_ROUTE_ITEMS,
303
+ env: import.meta.env as Record<string, string | undefined>
304
+ });
305
+ ```
306
+
307
+ ## Notas Operativas
308
+
309
+ - los warnings se emiten con `console.warn` desde runtime, backend list y agent builder.
310
+ - una funcion desconocida retorna payload estructurado `ok: false`.
311
+ - si el modulo de rutas falla o tiene shape invalido, el resolver hace fallback a rutas por defecto.
312
+
313
+ ## Documentacion Relacionada
314
+
315
+ - Version en espanol: `README.es.md`
316
+ - Version en ingles: `README.en.md`
317
+ - Paquete backend: `../voice-backend/README.md`
318
+ - Paquete mobile: `../voice-mobile/README.md`
319
+ - Playground Web: `../../apps/playground-web/README.md`
320
+ - Playground API: `../../apps/playground-api/README.md`
package/README.md CHANGED
@@ -1,126 +1,320 @@
1
- # @navai/voice-frontend
2
-
3
- Frontend helpers to integrate OpenAI Realtime voice without creating custom base plumbing in every app.
4
-
5
- ## What this package provides
6
-
7
- - Route resolution helpers.
8
- - Optional dynamic frontend function loader.
9
- - `useWebVoiceAgent(...)` to bootstrap voice session from React with backend + frontend tools.
10
- - `buildNavaiAgent(...)` with built-in tools:
11
- - `navigate_to`
12
- - `execute_app_function`
13
- - Optional backend function bridge:
14
- - frontend + backend functions can coexist under `execute_app_function`.
15
- - `createNavaiBackendClient(...)` to centralize calls to backend routes:
16
- - `POST /navai/realtime/client-secret`
17
- - `GET /navai/functions`
18
- - `POST /navai/functions/execute`
19
- - `resolveNavaiFrontendRuntimeConfig(...)` to read routes/functions from env.
20
-
21
- ## Install
22
-
23
- ```bash
24
- npm install @navai/voice-frontend @openai/agents zod
25
- ```
26
-
27
- Peer dependency for hooks:
28
-
29
- ```bash
30
- npm install react
31
- ```
32
-
33
- When installed from npm, this package auto-configures missing scripts in the consumer `package.json`:
34
-
35
- - `generate:module-loaders` -> `navai-generate-web-loaders`
36
- - `predev`, `prebuild`, `pretypecheck`, `prelint` -> `npm run generate:module-loaders`
37
-
38
- It only adds missing entries and never overwrites existing scripts.
39
- To disable auto-setup, set `NAVAI_SKIP_AUTO_SETUP=1` (or `NAVAI_SKIP_FRONTEND_AUTO_SETUP=1`) during install.
40
- To run setup manually later, use `npx navai-setup-voice-frontend`.
41
-
42
- ## Expected app inputs
43
-
44
- 1. Route data in `src/ai/routes.ts` (or any array compatible with `NavaiRoute[]`).
45
- 2. Optional function module loaders when you want local/frontend tools.
46
-
47
- ## Minimal usage (no bundler-specific APIs)
48
-
49
- ```ts
50
- import { buildNavaiAgent, createNavaiBackendClient } from "@navai/voice-frontend";
51
- import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
52
-
53
- const backendClient = createNavaiBackendClient({
54
- apiBaseUrl: "http://localhost:3000"
55
- });
56
- const backendFunctions = await backendClient.listFunctions();
57
-
58
- const { agent } = await buildNavaiAgent({
59
- navigate: (path) => routerNavigate(path),
60
- routes: NAVAI_ROUTE_ITEMS,
61
- backendFunctions: backendFunctions.functions,
62
- executeBackendFunction: backendClient.executeFunction
63
- });
64
- ```
65
-
66
- Then use `agent` with `RealtimeSession` from `@openai/agents/realtime`.
67
-
68
- ## React hook usage
69
-
70
- ```ts
71
- import { useWebVoiceAgent } from "@navai/voice-frontend";
72
- import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
73
- import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
74
-
75
- const agent = useWebVoiceAgent({
76
- navigate: (path) => routerNavigate(path),
77
- moduleLoaders: NAVAI_WEB_MODULE_LOADERS,
78
- defaultRoutes: NAVAI_ROUTE_ITEMS,
79
- env: import.meta.env as Record<string, string | undefined>
80
- });
81
- ```
82
-
83
- ## Optional dynamic frontend functions (bundler adapter)
84
-
85
- If your bundler can provide module loaders, you can add local frontend functions too.
86
-
87
- ```ts
88
- import { resolveNavaiFrontendRuntimeConfig } from "@navai/voice-frontend";
89
-
90
- // Vite adapter example (folder-scoped):
91
- const runtime = await resolveNavaiFrontendRuntimeConfig({
92
- moduleLoaders: import.meta.glob(["/src/ai/functions-modules/**/*.{ts,js}"]),
93
- defaultRoutes: NAVAI_ROUTE_ITEMS
94
- });
95
- ```
96
-
97
- Execution rule inside `execute_app_function`:
98
-
99
- - local/frontend function is attempted first.
100
- - if not found locally, backend function is attempted.
101
- - if names conflict, frontend function wins and backend one is ignored with warning.
102
-
103
- ## Runtime env keys
104
-
105
- `resolveNavaiFrontendRuntimeConfig` reads:
106
-
107
- - `NAVAI_ROUTES_FILE`
108
- - `NAVAI_FUNCTIONS_FOLDERS`
109
- - `NAVAI_REALTIME_MODEL`
110
-
111
- `createNavaiBackendClient` reads:
112
-
113
- - `NAVAI_API_URL` when you pass `env`
114
- - or `apiBaseUrl` directly (fallback `http://localhost:3000`)
115
-
116
- When `modelOverride` exists, pass it to:
117
-
118
- - backend request body: `POST /navai/realtime/client-secret`
119
- - realtime connection: `session.connect({ model })`
120
-
121
- ## `NAVAI_FUNCTIONS_FOLDERS` formats
122
-
123
- - single path: `src/ai/functions-modules`
124
- - recursive marker: `src/ai/functions-modules/...`
125
- - wildcard: `src/features/*/voice-functions`
126
- - CSV list: `src/ai/functions,src/features/account/functions`
1
+ # @navai/voice-frontend
2
+
3
+ <p>
4
+ <a href="./README.es.md"><img alt="Idioma Espanol" src="https://img.shields.io/badge/Idioma-ES-0A66C2?style=for-the-badge"></a>
5
+ <a href="./README.en.md"><img alt="Language English" src="https://img.shields.io/badge/Language-EN-1D9A6C?style=for-the-badge"></a>
6
+ </p>
7
+
8
+ Frontend package to build Navai voice agents in web applications.
9
+
10
+ It removes repeated boilerplate for:
11
+
12
+ 1. Realtime client secret requests.
13
+ 2. Route-aware navigation tools.
14
+ 3. Dynamic local function loading.
15
+ 4. Optional backend function bridging.
16
+ 5. React hook lifecycle for connect/disconnect.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @navai/voice-frontend @openai/agents zod
22
+ npm install react
23
+ ```
24
+
25
+ ## Package Architecture
26
+
27
+ This package is intentionally split by concern:
28
+
29
+ 1. `src/backend.ts`
30
+ HTTP client for backend routes:
31
+ - `POST /navai/realtime/client-secret`
32
+ - `GET /navai/functions`
33
+ - `POST /navai/functions/execute`
34
+
35
+ 2. `src/runtime.ts`
36
+ Runtime resolver for:
37
+ - route module selection
38
+ - function module filtering by `NAVAI_FUNCTIONS_FOLDERS`
39
+ - optional model override
40
+
41
+ 3. `src/functions.ts`
42
+ Local function loader:
43
+ - imports modules from generated loaders
44
+ - converts exports into normalized callable tool definitions
45
+
46
+ 4. `src/agent.ts`
47
+ Agent builder:
48
+ - creates `RealtimeAgent`
49
+ - injects built-in tools (`navigate_to`, `execute_app_function`)
50
+ - optionally adds direct alias tools for each allowed function
51
+
52
+ 5. `src/useWebVoiceAgent.ts`
53
+ React lifecycle wrapper:
54
+ - builds runtime config
55
+ - requests client secret
56
+ - discovers backend functions
57
+ - builds agent
58
+ - opens/closes `RealtimeSession`
59
+
60
+ 6. `src/routes.ts`
61
+ Route matching helpers for natural language to path resolution.
62
+
63
+ ## End-to-End Runtime Flow
64
+
65
+ Hook-driven runtime flow (`useWebVoiceAgent`):
66
+
67
+ 1. Resolve runtime config from `moduleLoaders` + `defaultRoutes` + env/options.
68
+ 2. Create backend client with `apiBaseUrl` or `NAVAI_API_URL`.
69
+ 3. On `start()`:
70
+ - request client secret.
71
+ - fetch backend function list.
72
+ - build Navai agent with local + backend functions.
73
+ - connect `RealtimeSession`.
74
+ 4. On `stop()`:
75
+ - close session and reset state.
76
+
77
+ State machine exposed by hook:
78
+
79
+ - `idle`
80
+ - `connecting`
81
+ - `connected`
82
+ - `error`
83
+
84
+ Agent voice state exposed by the hook:
85
+
86
+ - `agentVoiceState`: `idle | speaking`
87
+ - `isAgentSpeaking`: `boolean`
88
+
89
+ `agentVoiceState` is driven by realtime events `audio_start`, `audio_stopped`, and `audio_interrupted`.
90
+
91
+ ## Public API
92
+
93
+ Main exports:
94
+
95
+ - `buildNavaiAgent(...)`
96
+ - `createNavaiBackendClient(...)`
97
+ - `resolveNavaiFrontendRuntimeConfig(...)`
98
+ - `loadNavaiFunctions(...)`
99
+ - `useWebVoiceAgent(...)`
100
+ - `resolveNavaiRoute(...)`
101
+ - `getNavaiRoutePromptLines(...)`
102
+
103
+ Useful types:
104
+
105
+ - `NavaiRoute`
106
+ - `NavaiFunctionDefinition`
107
+ - `NavaiFunctionsRegistry`
108
+ - `NavaiBackendFunctionDefinition`
109
+ - `UseWebVoiceAgentOptions`
110
+
111
+ ## Tool Model and Behavior
112
+
113
+ `buildNavaiAgent` always registers:
114
+
115
+ - `navigate_to`
116
+ - `execute_app_function`
117
+
118
+ Optional direct alias tools:
119
+
120
+ - for each allowed function name, a direct tool can be created.
121
+ - reserved names are never used as direct tools (`navigate_to`, `execute_app_function`).
122
+ - invalid tool ids are skipped (kept accessible via `execute_app_function`).
123
+
124
+ Execution precedence:
125
+
126
+ 1. Try frontend/local function first.
127
+ 2. If missing, try backend function.
128
+ 3. If both exist with same name, frontend wins and backend is ignored with warning.
129
+
130
+ ## Dynamic Function Loading Internals
131
+
132
+ `loadNavaiFunctions` supports module export shapes:
133
+
134
+ 1. Exported function.
135
+ 2. Exported class (instance methods become functions).
136
+ 3. Exported object (callable members become functions).
137
+
138
+ Name normalization rules:
139
+
140
+ - snake_case lowercase.
141
+ - invalid chars removed.
142
+ - collisions are renamed with suffixes (`_2`, `_3`, ...).
143
+
144
+ Argument mapping rules:
145
+
146
+ - `payload.args` or `payload.arguments` as direct args.
147
+ - else `payload.value` as first arg.
148
+ - else full payload as first arg.
149
+ - context appended when arity indicates one more argument.
150
+
151
+ For class methods:
152
+
153
+ - constructor args: `payload.constructorArgs`.
154
+ - method args: `payload.methodArgs`.
155
+
156
+ ## Runtime Resolution and Env Precedence
157
+
158
+ `resolveNavaiFrontendRuntimeConfig` input priority:
159
+
160
+ 1. Explicit function args.
161
+ 2. Env object keys.
162
+ 3. Package defaults.
163
+
164
+ Keys used:
165
+
166
+ - `NAVAI_ROUTES_FILE`
167
+ - `NAVAI_FUNCTIONS_FOLDERS`
168
+ - `NAVAI_REALTIME_MODEL`
169
+
170
+ Defaults:
171
+
172
+ - routes file: `src/ai/routes.ts`
173
+ - functions folder: `src/ai/functions-modules`
174
+
175
+ Path matcher formats:
176
+
177
+ - folder: `src/ai/functions-modules`
178
+ - recursive: `src/ai/functions-modules/...`
179
+ - wildcard: `src/features/*/voice-functions`
180
+ - explicit file: `src/ai/functions-modules/secret.ts`
181
+ - CSV list: `a,b,c`
182
+
183
+ Fallback behavior:
184
+
185
+ - if configured folders match no modules, warning is emitted.
186
+ - resolver falls back to default functions folder.
187
+
188
+ ## Backend Client Behavior
189
+
190
+ `createNavaiBackendClient` base URL priority:
191
+
192
+ 1. `apiBaseUrl` option.
193
+ 2. `env.NAVAI_API_URL`.
194
+ 3. fallback `http://localhost:3000`.
195
+
196
+ Methods:
197
+
198
+ - `createClientSecret(input?)`
199
+ - `listFunctions()`
200
+ - `executeFunction({ functionName, payload })`
201
+
202
+ Error handling:
203
+
204
+ - network/HTTP failures throw for create/execute.
205
+ - function listing returns warnings and empty list on failures.
206
+
207
+ ## Generated Module Loader CLI
208
+
209
+ This package ships:
210
+
211
+ - `navai-generate-web-loaders`
212
+
213
+ Default command behavior:
214
+
215
+ 1. Reads `.env` and process env.
216
+ 2. Resolves `NAVAI_FUNCTIONS_FOLDERS` and `NAVAI_ROUTES_FILE`.
217
+ 3. Selects modules only from configured function paths.
218
+ 4. Optionally includes configured route module if it differs from default route module.
219
+ 5. Writes `src/ai/generated-module-loaders.ts`.
220
+
221
+ Manual usage:
222
+
223
+ ```bash
224
+ navai-generate-web-loaders
225
+ ```
226
+
227
+ Useful flags:
228
+
229
+ - `--project-root <path>`
230
+ - `--src-root <path>`
231
+ - `--output-file <path>`
232
+ - `--env-file <path>`
233
+ - `--default-functions-folder <path>`
234
+ - `--default-routes-file <path>`
235
+ - `--type-import <module>`
236
+ - `--export-name <identifier>`
237
+
238
+ ## Auto Setup on npm Install
239
+
240
+ Postinstall script can auto-add missing scripts in consumer app:
241
+
242
+ - `generate:module-loaders` -> `navai-generate-web-loaders`
243
+ - `predev` -> `npm run generate:module-loaders`
244
+ - `prebuild` -> `npm run generate:module-loaders`
245
+ - `pretypecheck` -> `npm run generate:module-loaders`
246
+ - `prelint` -> `npm run generate:module-loaders`
247
+
248
+ Rules:
249
+
250
+ - only missing scripts are added.
251
+ - existing scripts are never overwritten.
252
+
253
+ Disable auto setup:
254
+
255
+ - `NAVAI_SKIP_AUTO_SETUP=1`
256
+ - or `NAVAI_SKIP_FRONTEND_AUTO_SETUP=1`
257
+
258
+ Manual setup runner:
259
+
260
+ ```bash
261
+ npx navai-setup-voice-frontend
262
+ ```
263
+
264
+ ## Integration Examples
265
+
266
+ Imperative integration:
267
+
268
+ ```ts
269
+ import { RealtimeSession } from "@openai/agents/realtime";
270
+ import { buildNavaiAgent, createNavaiBackendClient } from "@navai/voice-frontend";
271
+ import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
272
+ import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
273
+
274
+ const backend = createNavaiBackendClient({ apiBaseUrl: "http://localhost:3000" });
275
+ const secret = await backend.createClientSecret();
276
+ const backendList = await backend.listFunctions();
277
+
278
+ const { agent, warnings } = await buildNavaiAgent({
279
+ navigate: (path) => router.navigate(path),
280
+ routes: NAVAI_ROUTE_ITEMS,
281
+ functionModuleLoaders: NAVAI_WEB_MODULE_LOADERS,
282
+ backendFunctions: backendList.functions,
283
+ executeBackendFunction: backend.executeFunction
284
+ });
285
+
286
+ warnings.forEach((w) => console.warn(w));
287
+
288
+ const session = new RealtimeSession(agent);
289
+ await session.connect({ apiKey: secret.value });
290
+ ```
291
+
292
+ React hook integration:
293
+
294
+ ```ts
295
+ import { useWebVoiceAgent } from "@navai/voice-frontend";
296
+ import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
297
+ import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
298
+
299
+ const voice = useWebVoiceAgent({
300
+ navigate: (path) => router.navigate(path),
301
+ moduleLoaders: NAVAI_WEB_MODULE_LOADERS,
302
+ defaultRoutes: NAVAI_ROUTE_ITEMS,
303
+ env: import.meta.env as Record<string, string | undefined>
304
+ });
305
+ ```
306
+
307
+ ## Operational Notes
308
+
309
+ - warnings are emitted with `console.warn` from runtime, backend list, and agent builder.
310
+ - unknown function execution returns structured `ok: false` payload.
311
+ - if route module fails to load or has invalid shape, resolver falls back to default routes.
312
+
313
+ ## Related Docs
314
+
315
+ - Spanish version: `README.es.md`
316
+ - English version: `README.en.md`
317
+ - Backend package: `../voice-backend/README.md`
318
+ - Mobile package: `../voice-mobile/README.md`
319
+ - Playground Web: `../../apps/playground-web/README.md`
320
+ - Playground API: `../../apps/playground-api/README.md`
package/dist/index.cjs CHANGED
@@ -760,6 +760,7 @@ function emitWarnings(warnings) {
760
760
  }
761
761
  function useWebVoiceAgent(options) {
762
762
  const sessionRef = (0, import_react.useRef)(null);
763
+ const attachedRealtimeSessionRef = (0, import_react.useRef)(null);
763
764
  const runtimeConfigPromise = (0, import_react.useMemo)(
764
765
  () => resolveNavaiFrontendRuntimeConfig({
765
766
  moduleLoaders: options.moduleLoaders,
@@ -790,15 +791,61 @@ function useWebVoiceAgent(options) {
790
791
  [options.apiBaseUrl, options.env]
791
792
  );
792
793
  const [status, setStatus] = (0, import_react.useState)("idle");
794
+ const [agentVoiceState, setAgentVoiceState] = (0, import_react.useState)("idle");
793
795
  const [error, setError] = (0, import_react.useState)(null);
796
+ const setAgentVoiceStateIfChanged = (0, import_react.useCallback)((next) => {
797
+ setAgentVoiceState((current) => current === next ? current : next);
798
+ }, []);
799
+ const handleSessionAudioStart = (0, import_react.useCallback)(() => {
800
+ setAgentVoiceStateIfChanged("speaking");
801
+ }, [setAgentVoiceStateIfChanged]);
802
+ const handleSessionAudioStopped = (0, import_react.useCallback)(() => {
803
+ setAgentVoiceStateIfChanged("idle");
804
+ }, [setAgentVoiceStateIfChanged]);
805
+ const handleSessionAudioInterrupted = (0, import_react.useCallback)(() => {
806
+ setAgentVoiceStateIfChanged("idle");
807
+ }, [setAgentVoiceStateIfChanged]);
808
+ const handleSessionError = (0, import_react.useCallback)(() => {
809
+ setAgentVoiceStateIfChanged("idle");
810
+ }, [setAgentVoiceStateIfChanged]);
811
+ const detachSessionAudioListeners = (0, import_react.useCallback)(() => {
812
+ const attachedSession = attachedRealtimeSessionRef.current;
813
+ if (!attachedSession) {
814
+ return;
815
+ }
816
+ attachedSession.off("audio_start", handleSessionAudioStart);
817
+ attachedSession.off("audio_stopped", handleSessionAudioStopped);
818
+ attachedSession.off("audio_interrupted", handleSessionAudioInterrupted);
819
+ attachedSession.off("error", handleSessionError);
820
+ attachedRealtimeSessionRef.current = null;
821
+ }, [handleSessionAudioInterrupted, handleSessionAudioStart, handleSessionAudioStopped, handleSessionError]);
822
+ const attachSessionAudioListeners = (0, import_react.useCallback)(
823
+ (session) => {
824
+ detachSessionAudioListeners();
825
+ session.on("audio_start", handleSessionAudioStart);
826
+ session.on("audio_stopped", handleSessionAudioStopped);
827
+ session.on("audio_interrupted", handleSessionAudioInterrupted);
828
+ session.on("error", handleSessionError);
829
+ attachedRealtimeSessionRef.current = session;
830
+ },
831
+ [
832
+ detachSessionAudioListeners,
833
+ handleSessionAudioInterrupted,
834
+ handleSessionAudioStart,
835
+ handleSessionAudioStopped,
836
+ handleSessionError
837
+ ]
838
+ );
794
839
  const stop = (0, import_react.useCallback)(() => {
840
+ detachSessionAudioListeners();
795
841
  try {
796
842
  sessionRef.current?.close();
797
843
  } finally {
798
844
  sessionRef.current = null;
799
845
  setStatus("idle");
846
+ setAgentVoiceStateIfChanged("idle");
800
847
  }
801
- }, []);
848
+ }, [detachSessionAudioListeners, setAgentVoiceStateIfChanged]);
802
849
  (0, import_react.useEffect)(() => {
803
850
  return () => {
804
851
  stop();
@@ -810,6 +857,7 @@ function useWebVoiceAgent(options) {
810
857
  }
811
858
  setError(null);
812
859
  setStatus("connecting");
860
+ setAgentVoiceStateIfChanged("idle");
813
861
  try {
814
862
  const runtimeConfig = await runtimeConfigPromise;
815
863
  const requestPayload = runtimeConfig.modelOverride ? { model: runtimeConfig.modelOverride } : {};
@@ -824,6 +872,7 @@ function useWebVoiceAgent(options) {
824
872
  });
825
873
  emitWarnings([...runtimeConfig.warnings, ...backendFunctionsResult.warnings, ...warnings]);
826
874
  const session = new import_realtime2.RealtimeSession(agent);
875
+ attachSessionAudioListeners(session);
827
876
  if (runtimeConfig.modelOverride) {
828
877
  await session.connect({ apiKey: secretPayload.value, model: runtimeConfig.modelOverride });
829
878
  } else {
@@ -835,18 +884,30 @@ function useWebVoiceAgent(options) {
835
884
  const message = formatError(startError);
836
885
  setError(message);
837
886
  setStatus("error");
887
+ setAgentVoiceStateIfChanged("idle");
888
+ detachSessionAudioListeners();
838
889
  try {
839
890
  sessionRef.current?.close();
840
891
  } catch {
841
892
  }
842
893
  sessionRef.current = null;
843
894
  }
844
- }, [backendClient, options.navigate, runtimeConfigPromise, status]);
895
+ }, [
896
+ attachSessionAudioListeners,
897
+ backendClient,
898
+ detachSessionAudioListeners,
899
+ options.navigate,
900
+ runtimeConfigPromise,
901
+ setAgentVoiceStateIfChanged,
902
+ status
903
+ ]);
845
904
  return {
846
905
  status,
906
+ agentVoiceState,
847
907
  error,
848
908
  isConnecting: status === "connecting",
849
909
  isConnected: status === "connected",
910
+ isAgentSpeaking: agentVoiceState === "speaking",
850
911
  start,
851
912
  stop
852
913
  };
package/dist/index.d.cts CHANGED
@@ -104,6 +104,7 @@ type ResolveNavaiFrontendRuntimeConfigResult = {
104
104
  declare function resolveNavaiFrontendRuntimeConfig(options: ResolveNavaiFrontendRuntimeConfigOptions): Promise<ResolveNavaiFrontendRuntimeConfigResult>;
105
105
 
106
106
  type VoiceStatus = "idle" | "connecting" | "connected" | "error";
107
+ type AgentVoiceState = "idle" | "speaking";
107
108
  type NavaiFrontendEnv = Record<string, string | undefined>;
108
109
  type UseWebVoiceAgentOptions = {
109
110
  navigate: (path: string) => void;
@@ -119,9 +120,11 @@ type UseWebVoiceAgentOptions = {
119
120
  };
120
121
  type UseWebVoiceAgentResult = {
121
122
  status: VoiceStatus;
123
+ agentVoiceState: AgentVoiceState;
122
124
  error: string | null;
123
125
  isConnecting: boolean;
124
126
  isConnected: boolean;
127
+ isAgentSpeaking: boolean;
125
128
  start: () => Promise<void>;
126
129
  stop: () => void;
127
130
  };
package/dist/index.d.ts CHANGED
@@ -104,6 +104,7 @@ type ResolveNavaiFrontendRuntimeConfigResult = {
104
104
  declare function resolveNavaiFrontendRuntimeConfig(options: ResolveNavaiFrontendRuntimeConfigOptions): Promise<ResolveNavaiFrontendRuntimeConfigResult>;
105
105
 
106
106
  type VoiceStatus = "idle" | "connecting" | "connected" | "error";
107
+ type AgentVoiceState = "idle" | "speaking";
107
108
  type NavaiFrontendEnv = Record<string, string | undefined>;
108
109
  type UseWebVoiceAgentOptions = {
109
110
  navigate: (path: string) => void;
@@ -119,9 +120,11 @@ type UseWebVoiceAgentOptions = {
119
120
  };
120
121
  type UseWebVoiceAgentResult = {
121
122
  status: VoiceStatus;
123
+ agentVoiceState: AgentVoiceState;
122
124
  error: string | null;
123
125
  isConnecting: boolean;
124
126
  isConnected: boolean;
127
+ isAgentSpeaking: boolean;
125
128
  start: () => Promise<void>;
126
129
  stop: () => void;
127
130
  };
package/dist/index.js CHANGED
@@ -728,6 +728,7 @@ function emitWarnings(warnings) {
728
728
  }
729
729
  function useWebVoiceAgent(options) {
730
730
  const sessionRef = useRef(null);
731
+ const attachedRealtimeSessionRef = useRef(null);
731
732
  const runtimeConfigPromise = useMemo(
732
733
  () => resolveNavaiFrontendRuntimeConfig({
733
734
  moduleLoaders: options.moduleLoaders,
@@ -758,15 +759,61 @@ function useWebVoiceAgent(options) {
758
759
  [options.apiBaseUrl, options.env]
759
760
  );
760
761
  const [status, setStatus] = useState("idle");
762
+ const [agentVoiceState, setAgentVoiceState] = useState("idle");
761
763
  const [error, setError] = useState(null);
764
+ const setAgentVoiceStateIfChanged = useCallback((next) => {
765
+ setAgentVoiceState((current) => current === next ? current : next);
766
+ }, []);
767
+ const handleSessionAudioStart = useCallback(() => {
768
+ setAgentVoiceStateIfChanged("speaking");
769
+ }, [setAgentVoiceStateIfChanged]);
770
+ const handleSessionAudioStopped = useCallback(() => {
771
+ setAgentVoiceStateIfChanged("idle");
772
+ }, [setAgentVoiceStateIfChanged]);
773
+ const handleSessionAudioInterrupted = useCallback(() => {
774
+ setAgentVoiceStateIfChanged("idle");
775
+ }, [setAgentVoiceStateIfChanged]);
776
+ const handleSessionError = useCallback(() => {
777
+ setAgentVoiceStateIfChanged("idle");
778
+ }, [setAgentVoiceStateIfChanged]);
779
+ const detachSessionAudioListeners = useCallback(() => {
780
+ const attachedSession = attachedRealtimeSessionRef.current;
781
+ if (!attachedSession) {
782
+ return;
783
+ }
784
+ attachedSession.off("audio_start", handleSessionAudioStart);
785
+ attachedSession.off("audio_stopped", handleSessionAudioStopped);
786
+ attachedSession.off("audio_interrupted", handleSessionAudioInterrupted);
787
+ attachedSession.off("error", handleSessionError);
788
+ attachedRealtimeSessionRef.current = null;
789
+ }, [handleSessionAudioInterrupted, handleSessionAudioStart, handleSessionAudioStopped, handleSessionError]);
790
+ const attachSessionAudioListeners = useCallback(
791
+ (session) => {
792
+ detachSessionAudioListeners();
793
+ session.on("audio_start", handleSessionAudioStart);
794
+ session.on("audio_stopped", handleSessionAudioStopped);
795
+ session.on("audio_interrupted", handleSessionAudioInterrupted);
796
+ session.on("error", handleSessionError);
797
+ attachedRealtimeSessionRef.current = session;
798
+ },
799
+ [
800
+ detachSessionAudioListeners,
801
+ handleSessionAudioInterrupted,
802
+ handleSessionAudioStart,
803
+ handleSessionAudioStopped,
804
+ handleSessionError
805
+ ]
806
+ );
762
807
  const stop = useCallback(() => {
808
+ detachSessionAudioListeners();
763
809
  try {
764
810
  sessionRef.current?.close();
765
811
  } finally {
766
812
  sessionRef.current = null;
767
813
  setStatus("idle");
814
+ setAgentVoiceStateIfChanged("idle");
768
815
  }
769
- }, []);
816
+ }, [detachSessionAudioListeners, setAgentVoiceStateIfChanged]);
770
817
  useEffect(() => {
771
818
  return () => {
772
819
  stop();
@@ -778,6 +825,7 @@ function useWebVoiceAgent(options) {
778
825
  }
779
826
  setError(null);
780
827
  setStatus("connecting");
828
+ setAgentVoiceStateIfChanged("idle");
781
829
  try {
782
830
  const runtimeConfig = await runtimeConfigPromise;
783
831
  const requestPayload = runtimeConfig.modelOverride ? { model: runtimeConfig.modelOverride } : {};
@@ -792,6 +840,7 @@ function useWebVoiceAgent(options) {
792
840
  });
793
841
  emitWarnings([...runtimeConfig.warnings, ...backendFunctionsResult.warnings, ...warnings]);
794
842
  const session = new RealtimeSession(agent);
843
+ attachSessionAudioListeners(session);
795
844
  if (runtimeConfig.modelOverride) {
796
845
  await session.connect({ apiKey: secretPayload.value, model: runtimeConfig.modelOverride });
797
846
  } else {
@@ -803,18 +852,30 @@ function useWebVoiceAgent(options) {
803
852
  const message = formatError(startError);
804
853
  setError(message);
805
854
  setStatus("error");
855
+ setAgentVoiceStateIfChanged("idle");
856
+ detachSessionAudioListeners();
806
857
  try {
807
858
  sessionRef.current?.close();
808
859
  } catch {
809
860
  }
810
861
  sessionRef.current = null;
811
862
  }
812
- }, [backendClient, options.navigate, runtimeConfigPromise, status]);
863
+ }, [
864
+ attachSessionAudioListeners,
865
+ backendClient,
866
+ detachSessionAudioListeners,
867
+ options.navigate,
868
+ runtimeConfigPromise,
869
+ setAgentVoiceStateIfChanged,
870
+ status
871
+ ]);
813
872
  return {
814
873
  status,
874
+ agentVoiceState,
815
875
  error,
816
876
  isConnecting: status === "connecting",
817
877
  isConnected: status === "connected",
878
+ isAgentSpeaking: agentVoiceState === "speaking",
818
879
  start,
819
880
  stop
820
881
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navai/voice-frontend",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Frontend helpers to build OpenAI Realtime voice agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -20,7 +20,9 @@
20
20
  "files": [
21
21
  "dist",
22
22
  "bin",
23
- "README.md"
23
+ "README.md",
24
+ "README.es.md",
25
+ "README.en.md"
24
26
  ],
25
27
  "scripts": {
26
28
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",