@eshal-bot/chat-widget 0.1.44 → 0.1.46
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/chat-widget.esm.js +5 -5
- package/dist/chat-widget.js +59 -29
- package/dist/chat-widget.min.js +5 -5
- package/dist/chat-widget.umd.js +5 -5
- package/package.json +1 -1
package/dist/chat-widget.js
CHANGED
|
@@ -8589,7 +8589,10 @@
|
|
|
8589
8589
|
* DEFAULT_SPEECH_GATE_CONFIG.
|
|
8590
8590
|
*
|
|
8591
8591
|
* @param {{ sampleRate: number, energyThreshold?: number, speechStartMs?: number, hangoverMs?: number, preRollMs?: number }} config
|
|
8592
|
-
* @param {{ onVoiceFrame: (pcm: ArrayBuffer) => void, onSpeechEnd: () => void }} callbacks
|
|
8592
|
+
* @param {{ onSpeechStart?: () => void, onVoiceFrame: (pcm: ArrayBuffer) => void, onSpeechEnd: () => void }} callbacks
|
|
8593
|
+
* onSpeechStart fires the instant a turn opens — use it for barge-in
|
|
8594
|
+
* (stop any assistant TTS still playing). onVoiceFrame emits each voiced
|
|
8595
|
+
* PCM frame; onSpeechEnd fires when the turn closes after the hangover.
|
|
8593
8596
|
*/
|
|
8594
8597
|
function createSpeechGate(config, callbacks) {
|
|
8595
8598
|
var _config$energyThresho, _config$speechStartMs, _config$hangoverMs, _config$preRollMs;
|
|
@@ -8637,10 +8640,14 @@
|
|
|
8637
8640
|
if (voiced) {
|
|
8638
8641
|
voicedRunMs += durationMs;
|
|
8639
8642
|
if (voicedRunMs >= cfg.speechStartMs) {
|
|
8640
|
-
|
|
8641
|
-
//
|
|
8643
|
+
var _callbacks$onSpeechSt;
|
|
8644
|
+
// Confirmed speech — open the turn. Fire onSpeechStart FIRST so the
|
|
8645
|
+
// caller can barge in (stop assistant TTS still playing) before we
|
|
8646
|
+
// start streaming this utterance, then flush the pre-roll (which
|
|
8647
|
+
// contains the voiced onset) and go live.
|
|
8642
8648
|
speaking = true;
|
|
8643
8649
|
silenceRunMs = 0;
|
|
8650
|
+
(_callbacks$onSpeechSt = callbacks.onSpeechStart) === null || _callbacks$onSpeechSt === void 0 || _callbacks$onSpeechSt.call(callbacks);
|
|
8644
8651
|
for (const frame of preRoll) callbacks.onVoiceFrame(frame.pcm);
|
|
8645
8652
|
preRoll = [];
|
|
8646
8653
|
preRollMsBuffered = 0;
|
|
@@ -9055,6 +9062,27 @@
|
|
|
9055
9062
|
};
|
|
9056
9063
|
};
|
|
9057
9064
|
|
|
9065
|
+
// Merge the text (SSE) and voice (Bidi) message arrays into one chronological
|
|
9066
|
+
// transcript: tag text messages with a default source, dedupe by id, and sort
|
|
9067
|
+
// by timestamp. Used for BOTH live display (activeMessages) and the
|
|
9068
|
+
// localStorage snapshot — so voice turns persist across reload exactly like
|
|
9069
|
+
// text turns. (Previously the snapshot saved only the text array, so voice
|
|
9070
|
+
// vanished on reload even though it rendered live and was saved server-side
|
|
9071
|
+
// in the Inbox.)
|
|
9072
|
+
const mergeTranscript = (messages, bidiMessages) => {
|
|
9073
|
+
const allMessages = [...messages.map(msg => _objectSpread2(_objectSpread2({}, msg), {}, {
|
|
9074
|
+
metadata: msg.metadata || {
|
|
9075
|
+
source: "sse"
|
|
9076
|
+
}
|
|
9077
|
+
})), ...bidiMessages];
|
|
9078
|
+
const uniqueMessages = Array.from(new Map(allMessages.map(msg => [msg.id, msg])).values());
|
|
9079
|
+
return uniqueMessages.sort((a, b) => {
|
|
9080
|
+
const timeA = a.timestamp instanceof Date ? a.timestamp.getTime() : new Date(a.timestamp).getTime();
|
|
9081
|
+
const timeB = b.timestamp instanceof Date ? b.timestamp.getTime() : new Date(b.timestamp).getTime();
|
|
9082
|
+
return timeA - timeB;
|
|
9083
|
+
});
|
|
9084
|
+
};
|
|
9085
|
+
|
|
9058
9086
|
// Helper functions for onboarding
|
|
9059
9087
|
const getOnboardingQuestions = onboardingQuestions => {
|
|
9060
9088
|
if (onboardingQuestions && onboardingQuestions.length > 0) {
|
|
@@ -9450,8 +9478,12 @@
|
|
|
9450
9478
|
reactExports.useEffect(() => {
|
|
9451
9479
|
if (!organizationId || !bidiSessionId) return;
|
|
9452
9480
|
if (isConversationLoading || isLoading) return;
|
|
9453
|
-
|
|
9454
|
-
|
|
9481
|
+
// Persist the MERGED transcript (text + voice). Previously only `messages`
|
|
9482
|
+
// (text) was saved, so bidi/voice turns — which live in `bidiMessages` —
|
|
9483
|
+
// were dropped on reload even though they render live and are saved
|
|
9484
|
+
// server-side (Inbox). Merging here makes voice survive reload like text.
|
|
9485
|
+
saveSessionMessages(organizationId, bidiSessionId, mergeTranscript(messages, bidiMessages), resolvedMessageLimits);
|
|
9486
|
+
}, [messages, bidiMessages, organizationId, bidiSessionId, isConversationLoading, isLoading, resolvedMessageLimits]);
|
|
9455
9487
|
|
|
9456
9488
|
// Show the onboarding form on first load; bypass it if already completed (persisted in session)
|
|
9457
9489
|
reactExports.useEffect(() => {
|
|
@@ -10581,6 +10613,25 @@
|
|
|
10581
10613
|
speechGateRef.current = createSpeechGate({
|
|
10582
10614
|
sampleRate: 16000
|
|
10583
10615
|
}, {
|
|
10616
|
+
// Barge-in: the instant the user starts talking, stop any assistant
|
|
10617
|
+
// TTS still playing so we don't talk over them. endOfAudio drops the
|
|
10618
|
+
// player's buffered audio immediately (readIndex → writeIndex). No-op
|
|
10619
|
+
// when nothing is playing (e.g. the user's first utterance).
|
|
10620
|
+
onSpeechStart: () => {
|
|
10621
|
+
var _audioPlayerNodeRef$c2;
|
|
10622
|
+
// Barge-in: user started talking → (1) stop assistant TTS already
|
|
10623
|
+
// buffered locally, and (2) tell the backend to cancel the in-flight
|
|
10624
|
+
// reply so its remaining TTS doesn't clobber the next turn's audio.
|
|
10625
|
+
(_audioPlayerNodeRef$c2 = audioPlayerNodeRef.current) === null || _audioPlayerNodeRef$c2 === void 0 || _audioPlayerNodeRef$c2.port.postMessage({
|
|
10626
|
+
command: "endOfAudio"
|
|
10627
|
+
});
|
|
10628
|
+
const ws = websocketRef.current;
|
|
10629
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
10630
|
+
ws.send(JSON.stringify({
|
|
10631
|
+
type: "interrupt"
|
|
10632
|
+
}));
|
|
10633
|
+
}
|
|
10634
|
+
},
|
|
10584
10635
|
onVoiceFrame: sendVoicePcm,
|
|
10585
10636
|
onSpeechEnd: () => {
|
|
10586
10637
|
const ws = websocketRef.current;
|
|
@@ -10753,11 +10804,11 @@
|
|
|
10753
10804
|
}, [isVoiceSessionActive]);
|
|
10754
10805
|
reactExports.useEffect(() => {
|
|
10755
10806
|
return () => {
|
|
10756
|
-
var _audioPlayerNodeRef$
|
|
10807
|
+
var _audioPlayerNodeRef$c3, _audioPlayerContextRe2, _audioRecorderNodeRef2, _audioRecorderContext2;
|
|
10757
10808
|
if (websocketRef.current && websocketRef.current.readyState === WebSocket.OPEN) {
|
|
10758
10809
|
websocketRef.current.close();
|
|
10759
10810
|
}
|
|
10760
|
-
(_audioPlayerNodeRef$
|
|
10811
|
+
(_audioPlayerNodeRef$c3 = audioPlayerNodeRef.current) === null || _audioPlayerNodeRef$c3 === void 0 || _audioPlayerNodeRef$c3.disconnect();
|
|
10761
10812
|
(_audioPlayerContextRe2 = audioPlayerContextRef.current) === null || _audioPlayerContextRe2 === void 0 || _audioPlayerContextRe2.close();
|
|
10762
10813
|
(_audioRecorderNodeRef2 = audioRecorderNodeRef.current) === null || _audioRecorderNodeRef2 === void 0 || _audioRecorderNodeRef2.disconnect();
|
|
10763
10814
|
(_audioRecorderContext2 = audioRecorderContextRef.current) === null || _audioRecorderContext2 === void 0 || _audioRecorderContext2.close();
|
|
@@ -10924,28 +10975,7 @@
|
|
|
10924
10975
|
|
|
10925
10976
|
// Merge and sort messages from both sources (text and voice) by timestamp
|
|
10926
10977
|
// This ensures all messages persist regardless of voice session state
|
|
10927
|
-
const activeMessages = reactExports.useMemo(() =>
|
|
10928
|
-
// Merge both message arrays
|
|
10929
|
-
const allMessages = [
|
|
10930
|
-
// Convert regular messages to include metadata if not present
|
|
10931
|
-
...messages.map(msg => _objectSpread2(_objectSpread2({}, msg), {}, {
|
|
10932
|
-
metadata: msg.metadata || {
|
|
10933
|
-
source: "sse"
|
|
10934
|
-
}
|
|
10935
|
-
})),
|
|
10936
|
-
// Include bidi messages
|
|
10937
|
-
...bidiMessages];
|
|
10938
|
-
|
|
10939
|
-
// Remove duplicates based on message ID (in case same message exists in both arrays)
|
|
10940
|
-
const uniqueMessages = Array.from(new Map(allMessages.map(msg => [msg.id, msg])).values());
|
|
10941
|
-
|
|
10942
|
-
// Sort by timestamp to maintain chronological order
|
|
10943
|
-
return uniqueMessages.sort((a, b) => {
|
|
10944
|
-
const timeA = a.timestamp instanceof Date ? a.timestamp.getTime() : new Date(a.timestamp).getTime();
|
|
10945
|
-
const timeB = b.timestamp instanceof Date ? b.timestamp.getTime() : new Date(b.timestamp).getTime();
|
|
10946
|
-
return timeA - timeB;
|
|
10947
|
-
});
|
|
10948
|
-
}, [bidiMessages, messages]);
|
|
10978
|
+
const activeMessages = reactExports.useMemo(() => mergeTranscript(messages, bidiMessages), [bidiMessages, messages]);
|
|
10949
10979
|
|
|
10950
10980
|
// Log message state changes for debugging
|
|
10951
10981
|
reactExports.useEffect(() => {
|