@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.1.7 → 0.1.9

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ChatBotContext.d.ts","sourceRoot":"","sources":["../../src/context/ChatBotContext.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAwB/B,OAAO,EAEL,kBAAkB,EAClB,YAAY,EAEb,MAAM,uBAAuB,CAAC;AAM/B,eAAO,MAAM,UAAU,0BAMtB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,YAAY,CA4mGlD,CAAC"}
1
+ {"version":3,"file":"ChatBotContext.d.ts","sourceRoot":"","sources":["../../src/context/ChatBotContext.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAyB/B,OAAO,EAEL,kBAAkB,EAClB,YAAY,EAEb,MAAM,uBAAuB,CAAC;AAM/B,eAAO,MAAM,UAAU,0BAMtB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,YAAY,CA+qGlD,CAAC"}
@@ -4,7 +4,7 @@ import stringify from 'fast-json-stable-stringify';
4
4
  import * as React from 'react';
5
5
  import { flushSync } from 'react-dom';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
- import { createNxtlinqApi, setApiHosts, synthesizeSpeechToBuffer, useLocalStorage, useSessionStorage, useSpeechToTextFromMic, useVoiceMode, metakeepClient, getEthers, sleep, walletTextUtils, } from '@bytexbyte/nxtlinq-ai-agent-web-development';
7
+ import { createNxtlinqApi, setApiHosts, synthesizeSpeechToBuffer, streamSpeechToAudioContext, useLocalStorage, useSessionStorage, useSpeechToTextFromMic, useVoiceMode, metakeepClient, getEthers, sleep, walletTextUtils, } from '@bytexbyte/nxtlinq-ai-agent-web-development';
8
8
  const MIC_ENABLED_SESSION_KEY = 'chatbot-mic-enabled';
9
9
  const ChatBotContext = React.createContext(undefined);
10
10
  export const useChatBot = () => {
@@ -88,6 +88,10 @@ piiDisplayMode = 'redacted', }) => {
88
88
  const audioCtxRef = React.useRef(null);
89
89
  const audioSourceRef = React.useRef(null);
90
90
  const audioElementRef = React.useRef(null);
91
+ // Streaming TTS (OpenAI PCM16 path) — tracks abort controller and all
92
+ // scheduled AudioBufferSourceNodes so stopTextToSpeech can cancel them.
93
+ const ttsAbortControllerRef = React.useRef(null);
94
+ const streamingSourcesRef = React.useRef([]);
91
95
  const speechingRef = React.useRef(false);
92
96
  const [isTtsProcessing, setIsTtsProcessing] = React.useState(false);
93
97
  const [requiresGesture, setRequiresGesture] = React.useState(false);
@@ -197,6 +201,58 @@ piiDisplayMode = 'redacted', }) => {
197
201
  setIsTtsProcessing(false);
198
202
  return;
199
203
  }
204
+ // OpenAI provider: stream PCM16 directly via fetch streaming so playback
205
+ // starts before the full audio is downloaded.
206
+ const provider = clientTtsVoiceRef.current?.provider ?? 'azure';
207
+ if (provider === 'openai') {
208
+ ttsAbortControllerRef.current?.abort();
209
+ ttsAbortControllerRef.current = null;
210
+ // Stop any still-playing nodes from the previous streaming call before
211
+ // clearing the ref — otherwise old sources keep playing and overlap.
212
+ for (const src of streamingSourcesRef.current) {
213
+ try {
214
+ src.stop();
215
+ src.disconnect();
216
+ }
217
+ catch { /* already ended */ }
218
+ }
219
+ streamingSourcesRef.current = [];
220
+ const abortController = new AbortController();
221
+ ttsAbortControllerRef.current = abortController;
222
+ // Always ensure a 24 kHz AudioContext for PCM16 streaming. Azure TTS
223
+ // uses the default OS rate (48 kHz), so reusing that context would
224
+ // pitch-shift the audio and cause the first chunk to sound distorted.
225
+ if (!audioCtxRef.current || audioCtxRef.current.state === 'closed' || audioCtxRef.current.sampleRate !== 24000) {
226
+ audioCtxRef.current = new AudioContext({ sampleRate: 24000 });
227
+ }
228
+ await streamSpeechToAudioContext({
229
+ text,
230
+ apiKey: apiKeyRef.current,
231
+ apiSecret: apiSecretRef.current,
232
+ audioCtx: audioCtxRef.current,
233
+ signal: abortController.signal,
234
+ onSourceScheduled: (source) => {
235
+ streamingSourcesRef.current.push(source);
236
+ },
237
+ onFirstChunk: () => {
238
+ if (abortController.signal.aborted)
239
+ return;
240
+ speechingRef.current = true;
241
+ if (messageIndex !== undefined)
242
+ setSpeechingIndex(messageIndex);
243
+ setIsTtsProcessing(false);
244
+ setRequiresGesture(false);
245
+ },
246
+ onEnded: () => {
247
+ if (abortController.signal.aborted)
248
+ return;
249
+ speechingRef.current = false;
250
+ setSpeechingIndex(undefined);
251
+ streamingSourcesRef.current = [];
252
+ },
253
+ });
254
+ return;
255
+ }
200
256
  const synth = await synthesizeSpeechToBuffer({
201
257
  text,
202
258
  apiKey: apiKeyRef.current,
@@ -2130,6 +2186,19 @@ piiDisplayMode = 'redacted', }) => {
2130
2186
  };
2131
2187
  // Stop text-to-speech (does not clear queue)
2132
2188
  const stopTextToSpeech = React.useCallback(() => {
2189
+ // Cancel any in-flight streaming TTS fetch and stop all scheduled nodes.
2190
+ if (ttsAbortControllerRef.current) {
2191
+ ttsAbortControllerRef.current.abort();
2192
+ ttsAbortControllerRef.current = null;
2193
+ }
2194
+ for (const source of streamingSourcesRef.current) {
2195
+ try {
2196
+ source.stop();
2197
+ source.disconnect();
2198
+ }
2199
+ catch { /* already stopped */ }
2200
+ }
2201
+ streamingSourcesRef.current = [];
2133
2202
  if (speechingRef.current) {
2134
2203
  // Stop AudioContext source if exists
2135
2204
  if (audioSourceRef.current) {
@@ -2176,6 +2245,9 @@ piiDisplayMode = 'redacted', }) => {
2176
2245
  getMessages: () => messagesRef.current,
2177
2246
  setMessages,
2178
2247
  onError,
2248
+ onToolCall: onToolUse
2249
+ ? (event) => { void onToolUse({ name: event.name, input: event.args }); }
2250
+ : undefined,
2179
2251
  stopRecording,
2180
2252
  stopTextToSpeech: stopTextToSpeechAndReset,
2181
2253
  voiceTransport: 'ws-realtime',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytexbyte/nxtlinq-ai-agent-ui-react-development",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Official React Web UI for nxtlinq AI Agent — drop-in chat widget",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -38,8 +38,8 @@
38
38
  "react-dom": ">=18.0.0"
39
39
  },
40
40
  "dependencies": {
41
- "@bytexbyte/nxtlinq-ai-agent-core-development": "0.4.1",
42
- "@bytexbyte/nxtlinq-ai-agent-web-development": "0.1.7",
41
+ "@bytexbyte/nxtlinq-ai-agent-core-development": "0.4.4",
42
+ "@bytexbyte/nxtlinq-ai-agent-web-development": "0.1.9",
43
43
  "@emotion/react": "^11.14.0",
44
44
  "@emotion/styled": "^11.14.1",
45
45
  "@mui/icons-material": "^7.2.0",
@@ -7,6 +7,7 @@ import {
7
7
  createNxtlinqApi,
8
8
  setApiHosts,
9
9
  synthesizeSpeechToBuffer,
10
+ streamSpeechToAudioContext,
10
11
  useLocalStorage,
11
12
  useSessionStorage,
12
13
  useSpeechToTextFromMic,
@@ -143,6 +144,10 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
143
144
  const audioCtxRef = React.useRef<AudioContext | null>(null);
144
145
  const audioSourceRef = React.useRef<AudioBufferSourceNode | null>(null);
145
146
  const audioElementRef = React.useRef<HTMLAudioElement | null>(null);
147
+ // Streaming TTS (OpenAI PCM16 path) — tracks abort controller and all
148
+ // scheduled AudioBufferSourceNodes so stopTextToSpeech can cancel them.
149
+ const ttsAbortControllerRef = React.useRef<AbortController | null>(null);
150
+ const streamingSourcesRef = React.useRef<AudioBufferSourceNode[]>([]);
146
151
  const speechingRef = React.useRef(false);
147
152
  const [isTtsProcessing, setIsTtsProcessing] = React.useState(false);
148
153
  const [requiresGesture, setRequiresGesture] = React.useState(false);
@@ -268,6 +273,56 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
268
273
  setIsTtsProcessing(false);
269
274
  return;
270
275
  }
276
+
277
+ // OpenAI provider: stream PCM16 directly via fetch streaming so playback
278
+ // starts before the full audio is downloaded.
279
+ const provider = clientTtsVoiceRef.current?.provider ?? 'azure';
280
+ if (provider === 'openai') {
281
+ ttsAbortControllerRef.current?.abort();
282
+ ttsAbortControllerRef.current = null;
283
+ // Stop any still-playing nodes from the previous streaming call before
284
+ // clearing the ref — otherwise old sources keep playing and overlap.
285
+ for (const src of streamingSourcesRef.current) {
286
+ try { src.stop(); src.disconnect(); } catch { /* already ended */ }
287
+ }
288
+ streamingSourcesRef.current = [];
289
+
290
+ const abortController = new AbortController();
291
+ ttsAbortControllerRef.current = abortController;
292
+
293
+ // Always ensure a 24 kHz AudioContext for PCM16 streaming. Azure TTS
294
+ // uses the default OS rate (48 kHz), so reusing that context would
295
+ // pitch-shift the audio and cause the first chunk to sound distorted.
296
+ if (!audioCtxRef.current || audioCtxRef.current.state === 'closed' || audioCtxRef.current.sampleRate !== 24000) {
297
+ audioCtxRef.current = new AudioContext({ sampleRate: 24000 });
298
+ }
299
+
300
+ await streamSpeechToAudioContext({
301
+ text,
302
+ apiKey: apiKeyRef.current,
303
+ apiSecret: apiSecretRef.current,
304
+ audioCtx: audioCtxRef.current,
305
+ signal: abortController.signal,
306
+ onSourceScheduled: (source: AudioBufferSourceNode) => {
307
+ streamingSourcesRef.current.push(source);
308
+ },
309
+ onFirstChunk: () => {
310
+ if (abortController.signal.aborted) return;
311
+ speechingRef.current = true;
312
+ if (messageIndex !== undefined) setSpeechingIndex(messageIndex);
313
+ setIsTtsProcessing(false);
314
+ setRequiresGesture(false);
315
+ },
316
+ onEnded: () => {
317
+ if (abortController.signal.aborted) return;
318
+ speechingRef.current = false;
319
+ setSpeechingIndex(undefined);
320
+ streamingSourcesRef.current = [];
321
+ },
322
+ });
323
+ return;
324
+ }
325
+
271
326
  const synth = await synthesizeSpeechToBuffer({
272
327
  text,
273
328
  apiKey: apiKeyRef.current,
@@ -2373,6 +2428,16 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2373
2428
 
2374
2429
  // Stop text-to-speech (does not clear queue)
2375
2430
  const stopTextToSpeech = React.useCallback(() => {
2431
+ // Cancel any in-flight streaming TTS fetch and stop all scheduled nodes.
2432
+ if (ttsAbortControllerRef.current) {
2433
+ ttsAbortControllerRef.current.abort();
2434
+ ttsAbortControllerRef.current = null;
2435
+ }
2436
+ for (const source of streamingSourcesRef.current) {
2437
+ try { source.stop(); source.disconnect(); } catch { /* already stopped */ }
2438
+ }
2439
+ streamingSourcesRef.current = [];
2440
+
2376
2441
  if (speechingRef.current) {
2377
2442
  // Stop AudioContext source if exists
2378
2443
  if (audioSourceRef.current) {
@@ -2432,6 +2497,9 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2432
2497
  getMessages: () => messagesRef.current,
2433
2498
  setMessages,
2434
2499
  onError,
2500
+ onToolCall: onToolUse
2501
+ ? (event) => { void onToolUse({ name: event.name, input: event.args }); }
2502
+ : undefined,
2435
2503
  stopRecording,
2436
2504
  stopTextToSpeech: stopTextToSpeechAndReset,
2437
2505
  voiceTransport: 'ws-realtime',