@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.1.6 → 0.1.8
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;
|
|
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,CA4qGlD,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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytexbyte/nxtlinq-ai-agent-ui-react-development",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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.
|
|
42
|
-
"@bytexbyte/nxtlinq-ai-agent-web-development": "0.1.
|
|
41
|
+
"@bytexbyte/nxtlinq-ai-agent-core-development": "0.4.2",
|
|
42
|
+
"@bytexbyte/nxtlinq-ai-agent-web-development": "0.1.8",
|
|
43
43
|
"@emotion/react": "^11.14.0",
|
|
44
44
|
"@emotion/styled": "^11.14.1",
|
|
45
45
|
"@mui/icons-material": "^7.2.0",
|
|
@@ -49,8 +49,6 @@
|
|
|
49
49
|
"uuid": "^11.1.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@bytexbyte/nxtlinq-ai-agent-core-development": "workspace:^",
|
|
53
|
-
"@bytexbyte/nxtlinq-ai-agent-web-development": "0.1.6",
|
|
54
52
|
"@types/react": "^18.2.64",
|
|
55
53
|
"@types/react-dom": "^18.2.25",
|
|
56
54
|
"react": "^18.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) {
|