@eshal-bot/chat-widget 0.2.41 → 0.2.42
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-MarkdownRenderer-BiM7QweB.esm.js → chat-widget-MarkdownRenderer-BNpESnAu.esm.js} +1 -1
- package/dist/chat-widget-index-LsKr3paz.esm.js +20 -0
- package/dist/chat-widget.esm.js +1 -1
- package/dist/chat-widget.js +141 -14
- package/dist/chat-widget.min.js +4 -4
- package/dist/chat-widget.umd.js +8 -8
- package/package.json +1 -1
- package/dist/chat-widget-index-i7Tp1iNw.esm.js +0 -20
package/dist/chat-widget.esm.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{S as default}from"./chat-widget-index-
|
|
1
|
+
export{S as default}from"./chat-widget-index-LsKr3paz.esm.js";import"react";import"react-dom/client";import"react/jsx-runtime";import"react-dom";
|
package/dist/chat-widget.js
CHANGED
|
@@ -8948,8 +8948,9 @@
|
|
|
8948
8948
|
* is a small energy-based state machine between the mic and the socket: it
|
|
8949
8949
|
* forwards audio ONLY inside a confirmed speech turn.
|
|
8950
8950
|
*
|
|
8951
|
-
* - idle -> speech needs
|
|
8952
|
-
* click, breath or keyboard tap never arms a turn
|
|
8951
|
+
* - idle -> speech needs `speechStartMs` of voiced energy inside the last
|
|
8952
|
+
* `speechWindowMs`, so a click, breath or keyboard tap never arms a turn,
|
|
8953
|
+
* while a quiet frame inside a short word does not throw the onset away;
|
|
8953
8954
|
* - on open it fires `onSpeechStart` (barge-in: stop assistant TTS), then
|
|
8954
8955
|
* flushes `preRollMs` of buffered audio so the first word is not clipped;
|
|
8955
8956
|
* - while speaking it forwards every frame, including short pauses;
|
|
@@ -8965,8 +8966,18 @@
|
|
|
8965
8966
|
// RMS energy (0..1) at/above which a frame counts as voiced. Speech is
|
|
8966
8967
|
// typically 0.05-0.3; quiet room tone sits well under 0.02.
|
|
8967
8968
|
energyThreshold: 0.025,
|
|
8968
|
-
//
|
|
8969
|
-
|
|
8969
|
+
// Voiced audio (ms) inside `speechWindowMs` required to OPEN a turn -
|
|
8970
|
+
// rejects coughs/clicks. EV-4952: this was 200 ms of strictly continuous
|
|
8971
|
+
// voiced frames, and on dev (4 Nov 2026) short answers like "yes" and
|
|
8972
|
+
// "hello" often never opened the gate: at 8 ms frames one quiet frame
|
|
8973
|
+
// between syllables reset the run, so the caller had to repeat themselves.
|
|
8974
|
+
speechStartMs: 120,
|
|
8975
|
+
// Sliding window (ms) the voiced audio above is counted over.
|
|
8976
|
+
speechWindowMs: 200,
|
|
8977
|
+
// Continuous quiet (ms) that abandons a half-built onset. A dip shorter
|
|
8978
|
+
// than this (a plosive, the gap inside "hel-lo") is tolerated; anything
|
|
8979
|
+
// longer is a real pause, so isolated pops cannot add up over time.
|
|
8980
|
+
onsetGapMs: 40,
|
|
8970
8981
|
// Continuous silence (ms) after speech that CLOSES the turn.
|
|
8971
8982
|
hangoverMs: 1000,
|
|
8972
8983
|
// Audio (ms) preceding turn-open to retain and flush.
|
|
@@ -8998,12 +9009,18 @@
|
|
|
8998
9009
|
sampleRate: config.sampleRate,
|
|
8999
9010
|
energyThreshold: pick(config.energyThreshold, DEFAULT_SPEECH_GATE_CONFIG.energyThreshold),
|
|
9000
9011
|
speechStartMs: pick(config.speechStartMs, DEFAULT_SPEECH_GATE_CONFIG.speechStartMs),
|
|
9012
|
+
speechWindowMs: pick(config.speechWindowMs, DEFAULT_SPEECH_GATE_CONFIG.speechWindowMs),
|
|
9013
|
+
onsetGapMs: pick(config.onsetGapMs, DEFAULT_SPEECH_GATE_CONFIG.onsetGapMs),
|
|
9001
9014
|
hangoverMs: pick(config.hangoverMs, DEFAULT_SPEECH_GATE_CONFIG.hangoverMs),
|
|
9002
9015
|
preRollMs: pick(config.preRollMs, DEFAULT_SPEECH_GATE_CONFIG.preRollMs)
|
|
9003
9016
|
};
|
|
9004
9017
|
let speaking = false;
|
|
9005
|
-
// While idle:
|
|
9006
|
-
|
|
9018
|
+
// While idle: recent frames ({ durationMs, voiced }) covering the last
|
|
9019
|
+
// `speechWindowMs`, with their voiced total, plus the current quiet run.
|
|
9020
|
+
let onset = [];
|
|
9021
|
+
let onsetMs = 0;
|
|
9022
|
+
let voicedMs = 0;
|
|
9023
|
+
let quietRunMs = 0;
|
|
9007
9024
|
// While speaking: how much consecutive silence has been seen (to close).
|
|
9008
9025
|
let silenceRunMs = 0;
|
|
9009
9026
|
// Ring of recent frames kept only while idle, for pre-roll on open.
|
|
@@ -9014,9 +9031,15 @@
|
|
|
9014
9031
|
const frameDurationMs = function (pcm) {
|
|
9015
9032
|
return pcm.byteLength / 2 / cfg.sampleRate * 1000;
|
|
9016
9033
|
};
|
|
9034
|
+
const clearOnset = function () {
|
|
9035
|
+
onset = [];
|
|
9036
|
+
onsetMs = 0;
|
|
9037
|
+
voicedMs = 0;
|
|
9038
|
+
quietRunMs = 0;
|
|
9039
|
+
};
|
|
9017
9040
|
const reset = function () {
|
|
9018
9041
|
speaking = false;
|
|
9019
|
-
|
|
9042
|
+
clearOnset();
|
|
9020
9043
|
silenceRunMs = 0;
|
|
9021
9044
|
preRoll = [];
|
|
9022
9045
|
preRollMsBuffered = 0;
|
|
@@ -9036,22 +9059,35 @@
|
|
|
9036
9059
|
const dropped = preRoll.shift();
|
|
9037
9060
|
preRollMsBuffered -= dropped ? dropped.durationMs : 0;
|
|
9038
9061
|
}
|
|
9062
|
+
onset.push({
|
|
9063
|
+
durationMs: durationMs,
|
|
9064
|
+
voiced: voiced
|
|
9065
|
+
});
|
|
9066
|
+
onsetMs += durationMs;
|
|
9067
|
+
if (voiced) voicedMs += durationMs;
|
|
9068
|
+
while (onsetMs > cfg.speechWindowMs && onset.length > 1) {
|
|
9069
|
+
const old = onset.shift();
|
|
9070
|
+
onsetMs -= old.durationMs;
|
|
9071
|
+
if (old.voiced) voicedMs -= old.durationMs;
|
|
9072
|
+
}
|
|
9039
9073
|
if (voiced) {
|
|
9040
|
-
|
|
9041
|
-
if (
|
|
9074
|
+
quietRunMs = 0;
|
|
9075
|
+
if (voicedMs >= cfg.speechStartMs) {
|
|
9042
9076
|
// Confirmed speech: open the turn. onSpeechStart first so the
|
|
9043
9077
|
// caller can barge in, then flush the pre-roll and go live.
|
|
9044
9078
|
speaking = true;
|
|
9045
9079
|
silenceRunMs = 0;
|
|
9080
|
+
clearOnset();
|
|
9046
9081
|
if (typeof callbacks.onSpeechStart === 'function') callbacks.onSpeechStart();
|
|
9047
9082
|
for (let i = 0; i < preRoll.length; i += 1) callbacks.onVoiceFrame(preRoll[i].pcm);
|
|
9048
9083
|
preRoll = [];
|
|
9049
9084
|
preRollMsBuffered = 0;
|
|
9050
9085
|
}
|
|
9051
9086
|
} else {
|
|
9052
|
-
//
|
|
9053
|
-
// transient cannot accumulate its way over the threshold.
|
|
9054
|
-
|
|
9087
|
+
// A brief dip is tolerated; a real pause abandons the onset so a
|
|
9088
|
+
// stray transient cannot accumulate its way over the threshold.
|
|
9089
|
+
quietRunMs += durationMs;
|
|
9090
|
+
if (quietRunMs >= cfg.onsetGapMs) clearOnset();
|
|
9055
9091
|
}
|
|
9056
9092
|
return;
|
|
9057
9093
|
}
|
|
@@ -9065,7 +9101,7 @@
|
|
|
9065
9101
|
silenceRunMs += durationMs;
|
|
9066
9102
|
if (silenceRunMs >= cfg.hangoverMs) {
|
|
9067
9103
|
speaking = false;
|
|
9068
|
-
|
|
9104
|
+
clearOnset();
|
|
9069
9105
|
silenceRunMs = 0;
|
|
9070
9106
|
callbacks.onSpeechEnd();
|
|
9071
9107
|
}
|
|
@@ -12278,6 +12314,54 @@
|
|
|
12278
12314
|
return "voiceUnavailable";
|
|
12279
12315
|
}
|
|
12280
12316
|
|
|
12317
|
+
/**
|
|
12318
|
+
* The spoken greeting voice-runtime sends as soon as a call connects (EV-4952).
|
|
12319
|
+
*
|
|
12320
|
+
* The server sends `{"greeting": {"text": "..."}}` once, before the greeting
|
|
12321
|
+
* audio, then the audio frames as usual, then `turnComplete`. Until now the
|
|
12322
|
+
* widget dropped every bot reply before the user had spoken, so on the dev
|
|
12323
|
+
* call UR-0016 (Aurelia org org_1793698095653_c341070ff1, 4 Nov 2026) the
|
|
12324
|
+
* caller sat in silence until they spoke first. The marker is what lets the
|
|
12325
|
+
* client tell the greeting apart from a stray reply and play it.
|
|
12326
|
+
*
|
|
12327
|
+
* A client that never gets the marker behaves exactly as before. Mirrors
|
|
12328
|
+
* packages/frontend/src/components/chatbot-preview/voiceGreeting.ts.
|
|
12329
|
+
*/
|
|
12330
|
+
|
|
12331
|
+
/** The greeting text if the event is the greeting marker, else null. */
|
|
12332
|
+
function voiceGreetingText(event) {
|
|
12333
|
+
const text = event?.greeting?.text;
|
|
12334
|
+
if (typeof text !== "string") return null;
|
|
12335
|
+
const trimmed = text.trim();
|
|
12336
|
+
return trimmed === "" ? null : trimmed;
|
|
12337
|
+
}
|
|
12338
|
+
|
|
12339
|
+
/**
|
|
12340
|
+
* The bubbles after the greeting arrives.
|
|
12341
|
+
*
|
|
12342
|
+
* - The static welcome this call added (`welcomeId`) is replaced by the
|
|
12343
|
+
* greeting, so the call does not show both.
|
|
12344
|
+
* - A bubble already on screen with the same text (a welcome shown before the
|
|
12345
|
+
* call started, in either list) is kept and no duplicate is added.
|
|
12346
|
+
* - Otherwise the greeting is appended as the bot's bubble.
|
|
12347
|
+
*/
|
|
12348
|
+
function applyVoiceGreeting(prev, text, welcomeId, others, makeBubble) {
|
|
12349
|
+
if (welcomeId) {
|
|
12350
|
+
const at = prev.findIndex(m => m.id === welcomeId);
|
|
12351
|
+
if (at >= 0) {
|
|
12352
|
+
const next = [...prev];
|
|
12353
|
+
next[at] = {
|
|
12354
|
+
...next[at],
|
|
12355
|
+
content: text
|
|
12356
|
+
};
|
|
12357
|
+
return next;
|
|
12358
|
+
}
|
|
12359
|
+
}
|
|
12360
|
+
const shown = [...prev, ...(others || [])].some(m => m.role === "assistant" && typeof m.content === "string" && m.content.trim() === text);
|
|
12361
|
+
if (shown) return prev;
|
|
12362
|
+
return [...prev, makeBubble(text)];
|
|
12363
|
+
}
|
|
12364
|
+
|
|
12281
12365
|
/**
|
|
12282
12366
|
* Replies the widget never streamed itself.
|
|
12283
12367
|
*
|
|
@@ -13267,6 +13351,14 @@
|
|
|
13267
13351
|
const lastProcessedAssistantContentRef = reactExports.useRef(null); // Track last processed content to prevent duplicates
|
|
13268
13352
|
const userHasSpokenRef = reactExports.useRef(false); // Track if user has sent first message - prevents automatic greetings
|
|
13269
13353
|
const turnHasTranscriptionRef = reactExports.useRef(false); // True when outputTranscription received this turn - skip content.parts text
|
|
13354
|
+
// EV-4952: true from the server's greeting marker until its turnComplete
|
|
13355
|
+
// (or a barge-in), so the greeting audio plays before the user has spoken.
|
|
13356
|
+
const greetingActiveRef = reactExports.useRef(false);
|
|
13357
|
+
const voiceWelcomeIdRef = reactExports.useRef(null); // the static welcome bubble this voice call added, if any
|
|
13358
|
+
// The text-mode bubbles, read by the greeting de-dup without re-binding the
|
|
13359
|
+
// socket handler on every message.
|
|
13360
|
+
const voiceTextMessagesRef = reactExports.useRef(messages);
|
|
13361
|
+
voiceTextMessagesRef.current = messages;
|
|
13270
13362
|
const stopVoiceSessionRef = reactExports.useRef(null); // Ref to avoid referencing stopVoiceSession before it is defined in handleBidiEvent
|
|
13271
13363
|
const pendingVoiceSourcesRef = reactExports.useRef(null); // Hold sources until the assistant bubble for this turn exists
|
|
13272
13364
|
// Anonymous browser fingerprint from @eshal-tracker/sdk. Resolved
|
|
@@ -14998,6 +15090,7 @@
|
|
|
14998
15090
|
currentAssistantMessageIdRef.current = null;
|
|
14999
15091
|
currentInputMessageIdRef.current = null;
|
|
15000
15092
|
turnHasTranscriptionRef.current = false; // reset for next turn
|
|
15093
|
+
greetingActiveRef.current = false; // the greeting turn, if any, is over
|
|
15001
15094
|
pendingVoiceSourcesRef.current = null;
|
|
15002
15095
|
}, []);
|
|
15003
15096
|
const handleInterrupted = reactExports.useCallback(() => {
|
|
@@ -15009,6 +15102,7 @@
|
|
|
15009
15102
|
}
|
|
15010
15103
|
currentAssistantMessageIdRef.current = null;
|
|
15011
15104
|
currentInputMessageIdRef.current = null;
|
|
15105
|
+
greetingActiveRef.current = false; // a barge-in over the greeting ends it
|
|
15012
15106
|
pendingVoiceSourcesRef.current = null;
|
|
15013
15107
|
setBidiMessages(prev => prev.map(message => message.metadata?.source === "bidi" ? {
|
|
15014
15108
|
...message,
|
|
@@ -15226,6 +15320,35 @@
|
|
|
15226
15320
|
return;
|
|
15227
15321
|
}
|
|
15228
15322
|
|
|
15323
|
+
// EV-4952: voice-runtime greets the caller as soon as the call connects
|
|
15324
|
+
// and marks it with {"greeting": {"text"}} before the audio. Show the
|
|
15325
|
+
// text as the bot's bubble now (replacing this call's static welcome,
|
|
15326
|
+
// never duplicating one already on screen) and let the audio that
|
|
15327
|
+
// follows play until turnComplete, even though the user has not spoken.
|
|
15328
|
+
const greetingText = voiceGreetingText(event);
|
|
15329
|
+
if (greetingText) {
|
|
15330
|
+
greetingActiveRef.current = true;
|
|
15331
|
+
setBidiMessages(prev => applyVoiceGreeting(prev, greetingText, voiceWelcomeIdRef.current, voiceTextMessagesRef.current, text => createMessage({
|
|
15332
|
+
id: `bidi-greeting-${Date.now()}`,
|
|
15333
|
+
role: "assistant",
|
|
15334
|
+
content: text,
|
|
15335
|
+
metadata: {
|
|
15336
|
+
source: "bidi"
|
|
15337
|
+
}
|
|
15338
|
+
})));
|
|
15339
|
+
return;
|
|
15340
|
+
}
|
|
15341
|
+
if (greetingActiveRef.current && !userHasSpokenRef.current) {
|
|
15342
|
+
// The greeting's text is already on screen: play its audio only.
|
|
15343
|
+
if (event.content?.parts) {
|
|
15344
|
+
handleContentParts(event.content.parts, Boolean(event.content?.finished || event.finished), true);
|
|
15345
|
+
}
|
|
15346
|
+
if (event.turnComplete) {
|
|
15347
|
+
handleTurnComplete();
|
|
15348
|
+
}
|
|
15349
|
+
return;
|
|
15350
|
+
}
|
|
15351
|
+
|
|
15229
15352
|
// CRITICAL: Ignore assistant responses until user has spoken
|
|
15230
15353
|
// This prevents automatic greetings from appearing before user's first message
|
|
15231
15354
|
if (!userHasSpokenRef.current) {
|
|
@@ -15523,6 +15646,8 @@
|
|
|
15523
15646
|
|
|
15524
15647
|
// Reset userHasSpoken flag when starting new session
|
|
15525
15648
|
userHasSpokenRef.current = false;
|
|
15649
|
+
greetingActiveRef.current = false;
|
|
15650
|
+
voiceWelcomeIdRef.current = null;
|
|
15526
15651
|
setBidiMessages(prev => {
|
|
15527
15652
|
// Don't add welcome message if there are already any messages (either bidi or text)
|
|
15528
15653
|
// This prevents showing welcome message when turning voice on again after a conversation
|
|
@@ -15543,6 +15668,8 @@
|
|
|
15543
15668
|
source: "bidi"
|
|
15544
15669
|
}
|
|
15545
15670
|
});
|
|
15671
|
+
// Remembered so a spoken greeting (EV-4952) replaces it, not doubles it.
|
|
15672
|
+
voiceWelcomeIdRef.current = welcomeMsg.id;
|
|
15546
15673
|
return [welcomeMsg];
|
|
15547
15674
|
});
|
|
15548
15675
|
connectWebsocket();
|
|
@@ -23285,7 +23412,7 @@
|
|
|
23285
23412
|
* value by its own test (that constant sat at 0.1.113 while this package was
|
|
23286
23413
|
* at 0.2.2 precisely because nothing enforced it - EV-2431).
|
|
23287
23414
|
*/
|
|
23288
|
-
const WIDGET_VERSION = '0.2.
|
|
23415
|
+
const WIDGET_VERSION = '0.2.42';
|
|
23289
23416
|
|
|
23290
23417
|
/**
|
|
23291
23418
|
* API utility functions
|