@eshal-bot/chat-widget 0.2.17 → 0.2.19
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-D0VG_NOg.esm.js → chat-widget-MarkdownRenderer-D50nDQ4t.esm.js} +6 -6
- package/dist/chat-widget-index-ClEAcv15.esm.js +20 -0
- package/dist/chat-widget-markdown-chunk.js +28 -13
- package/dist/chat-widget-markdown-chunk.min.js +1 -1
- package/dist/chat-widget.esm.js +1 -1
- package/dist/chat-widget.js +147 -30
- package/dist/chat-widget.min.js +6 -6
- package/dist/chat-widget.umd.js +8 -8
- package/package.json +1 -1
- package/dist/chat-widget-index-CNSzbZUh.esm.js +0 -20
package/dist/chat-widget.esm.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{e as default}from"./chat-widget-index-
|
|
1
|
+
export{e as default}from"./chat-widget-index-ClEAcv15.esm.js";import"react";import"react-dom/client";import"react/jsx-runtime";import"react-dom";
|
package/dist/chat-widget.js
CHANGED
|
@@ -8798,9 +8798,21 @@
|
|
|
8798
8798
|
this.buffer = new Float32Array(this.bufferSize);
|
|
8799
8799
|
this.writeIndex = 0;
|
|
8800
8800
|
this.readIndex = 0;
|
|
8801
|
+
// Paused while the caller may be speaking over the reply (lib/audio/bargeIn.js).
|
|
8802
|
+
this.paused = false;
|
|
8801
8803
|
this.port.onmessage = (event) => {
|
|
8802
|
-
|
|
8804
|
+
const command = event.data?.command;
|
|
8805
|
+
if (command === 'endOfAudio') {
|
|
8803
8806
|
this.readIndex = this.writeIndex;
|
|
8807
|
+
this.paused = false;
|
|
8808
|
+
return;
|
|
8809
|
+
}
|
|
8810
|
+
if (command === 'pause') {
|
|
8811
|
+
this.paused = true;
|
|
8812
|
+
return;
|
|
8813
|
+
}
|
|
8814
|
+
if (command === 'resume') {
|
|
8815
|
+
this.paused = false;
|
|
8804
8816
|
return;
|
|
8805
8817
|
}
|
|
8806
8818
|
const int16Samples = new Int16Array(event.data);
|
|
@@ -8823,11 +8835,14 @@
|
|
|
8823
8835
|
const output = outputs[0];
|
|
8824
8836
|
const framesPerBlock = output[0].length;
|
|
8825
8837
|
for (let frame = 0; frame < framesPerBlock; frame += 1) {
|
|
8826
|
-
|
|
8838
|
+
// Silence when paused or with nothing buffered.
|
|
8839
|
+
const playing = !this.paused && this.readIndex !== this.writeIndex;
|
|
8840
|
+
const sample = playing ? this.buffer[this.readIndex] : 0;
|
|
8841
|
+
output[0][frame] = sample;
|
|
8827
8842
|
if (output.length > 1) {
|
|
8828
|
-
output[1][frame] =
|
|
8843
|
+
output[1][frame] = sample;
|
|
8829
8844
|
}
|
|
8830
|
-
if (
|
|
8845
|
+
if (playing) {
|
|
8831
8846
|
this.readIndex = (this.readIndex + 1) % this.bufferSize;
|
|
8832
8847
|
}
|
|
8833
8848
|
}
|
|
@@ -8884,9 +8899,16 @@
|
|
|
8884
8899
|
|
|
8885
8900
|
// Clean up the Blob URL after the worklet is loaded
|
|
8886
8901
|
URL.revokeObjectURL(workletUrl);
|
|
8902
|
+
|
|
8903
|
+
// The reply plays through the speakers while the mic is open: without echo
|
|
8904
|
+
// cancellation it comes back in, and the voice gate takes it for the caller
|
|
8905
|
+
// barging in (dev, 16 Oct 2026).
|
|
8887
8906
|
const micStream = await navigator.mediaDevices.getUserMedia({
|
|
8888
8907
|
audio: {
|
|
8889
|
-
channelCount: 1
|
|
8908
|
+
channelCount: 1,
|
|
8909
|
+
echoCancellation: true,
|
|
8910
|
+
noiseSuppression: true,
|
|
8911
|
+
autoGainControl: true
|
|
8890
8912
|
}
|
|
8891
8913
|
});
|
|
8892
8914
|
const source = audioRecorderContext.createMediaStreamSource(micStream);
|
|
@@ -9059,6 +9081,34 @@
|
|
|
9059
9081
|
};
|
|
9060
9082
|
}
|
|
9061
9083
|
|
|
9084
|
+
/**
|
|
9085
|
+
* What becomes of a reply's audio when the caller may be speaking over it.
|
|
9086
|
+
*
|
|
9087
|
+
* The voice gate hears sound over a reply (the caller, a cough, the reply
|
|
9088
|
+
* itself through the speakers). It used to empty the player at once, so
|
|
9089
|
+
* everything already buffered was lost; the server sends audio faster than it
|
|
9090
|
+
* plays, so that was most of the reply. Dev, 16 Oct 2026 (Greenleaf voice): a
|
|
9091
|
+
* noise during the checkout reply cut it off, and the server, finding no
|
|
9092
|
+
* speech, sent only the audio it had still been holding.
|
|
9093
|
+
*
|
|
9094
|
+
* Now the player pauses, and the server's verdict on what it heard decides:
|
|
9095
|
+
* an empty transcription is no speech (resume), the caller's words or
|
|
9096
|
+
* `interrupted` are a real barge-in (discard). With no verdict at all the
|
|
9097
|
+
* reply resumes BARGE_IN_RESUME_AFTER_MS after the sound ends.
|
|
9098
|
+
* Mirrors packages/frontend/src/lib/audio/bargeIn.ts.
|
|
9099
|
+
*/
|
|
9100
|
+
const BARGE_IN_RESUME_AFTER_MS = 4000;
|
|
9101
|
+
|
|
9102
|
+
/** "resume" | "discard" | null (the event says nothing about the sound). */
|
|
9103
|
+
function bargeInVerdict(event) {
|
|
9104
|
+
if (!event) return null;
|
|
9105
|
+
if (event.interrupted) return 'discard';
|
|
9106
|
+
const heard = event.inputTranscription;
|
|
9107
|
+
if (!heard) return null;
|
|
9108
|
+
if (String(heard.text ?? '').trim()) return 'discard';
|
|
9109
|
+
return heard.finished ? 'resume' : null;
|
|
9110
|
+
}
|
|
9111
|
+
|
|
9062
9112
|
// Lightweight i18n for the embeddable chat widget.
|
|
9063
9113
|
// Translations are bundled so the widget stays dependency-free.
|
|
9064
9114
|
// Locale is resolved from `defaultLanguage` (e.g. "zh", "ar") configured
|
|
@@ -11392,6 +11442,31 @@
|
|
|
11392
11442
|
return "voiceUnavailable";
|
|
11393
11443
|
}
|
|
11394
11444
|
|
|
11445
|
+
/**
|
|
11446
|
+
* Replies the widget never streamed itself.
|
|
11447
|
+
*
|
|
11448
|
+
* The widget holds its own copy of every /support turn, under its own ids, so
|
|
11449
|
+
* on reload (and on the event stream's backfill) it merges from the server
|
|
11450
|
+
* only what reached the conversation another way: what a person sent from the
|
|
11451
|
+
* Inbox (contentType human_reply, handoff_ack) and a Skill's reply to a staff
|
|
11452
|
+
* turn (staffTurn: Instruct Skill, or a hand-back that resumes the Skill;
|
|
11453
|
+
* backend services/staffTurn.ts). Dev, 15 Oct 2026: a Skill reply sent while
|
|
11454
|
+
* the Testing lab was closed never showed there, because only a person's
|
|
11455
|
+
* replies were merged. Mirrors packages/frontend/src/lib/messages/repliedOutsideStream.ts.
|
|
11456
|
+
*/
|
|
11457
|
+
function isRepliedOutsideStream(m) {
|
|
11458
|
+
return m?.contentType === 'human_reply' || m?.contentType === 'handoff_ack' || m?.staffTurn === true;
|
|
11459
|
+
}
|
|
11460
|
+
|
|
11461
|
+
/**
|
|
11462
|
+
* Who owns the conversation once a pushed message is out, or null when the
|
|
11463
|
+
* message does not change that. A staff turn's reply says: the AI answers
|
|
11464
|
+
* again, or a person still has it when the Skill handed off again.
|
|
11465
|
+
*/
|
|
11466
|
+
function responderAfterPush(m) {
|
|
11467
|
+
return m?.responder === 'ai' || m?.responder === 'human' ? m.responder : null;
|
|
11468
|
+
}
|
|
11469
|
+
|
|
11395
11470
|
/**
|
|
11396
11471
|
* Clean CJK (Chinese, Japanese, Korean) spaces
|
|
11397
11472
|
*/
|
|
@@ -12182,11 +12257,31 @@
|
|
|
12182
12257
|
isHandedOffRef.current = true;
|
|
12183
12258
|
setIsHandedOff(true);
|
|
12184
12259
|
}, []);
|
|
12260
|
+
// The AI owns the conversation again: a Skill replied to an Instruct Skill.
|
|
12261
|
+
// Left handed off, the customer's next message was sent and the AI's
|
|
12262
|
+
// answer to it drained and dropped below.
|
|
12263
|
+
const markAiOwned = reactExports.useCallback(() => {
|
|
12264
|
+
isHandedOffRef.current = false;
|
|
12265
|
+
setIsHandedOff(false);
|
|
12266
|
+
}, []);
|
|
12185
12267
|
|
|
12186
12268
|
// Bidi refs
|
|
12187
12269
|
const websocketRef = reactExports.useRef(null);
|
|
12188
12270
|
const audioPlayerNodeRef = reactExports.useRef(null);
|
|
12189
12271
|
const audioPlayerContextRef = reactExports.useRef(null);
|
|
12272
|
+
// Sound over a reply pauses its audio until the server says what it heard
|
|
12273
|
+
// (lib/audio/bargeIn.js); the timer resumes it when no verdict comes.
|
|
12274
|
+
const bargeInPendingRef = reactExports.useRef(false);
|
|
12275
|
+
const bargeInTimerRef = reactExports.useRef(null);
|
|
12276
|
+
const settleBargeIn = reactExports.useCallback(verdict => {
|
|
12277
|
+
if (bargeInTimerRef.current) clearTimeout(bargeInTimerRef.current);
|
|
12278
|
+
bargeInTimerRef.current = null;
|
|
12279
|
+
if (!bargeInPendingRef.current) return;
|
|
12280
|
+
bargeInPendingRef.current = false;
|
|
12281
|
+
audioPlayerNodeRef.current?.port.postMessage({
|
|
12282
|
+
command: verdict === "resume" ? "resume" : "endOfAudio"
|
|
12283
|
+
});
|
|
12284
|
+
}, []);
|
|
12190
12285
|
const audioRecorderNodeRef = reactExports.useRef(null);
|
|
12191
12286
|
const audioRecorderContextRef = reactExports.useRef(null);
|
|
12192
12287
|
const micStreamRef = reactExports.useRef(null);
|
|
@@ -12321,7 +12416,7 @@
|
|
|
12321
12416
|
if (!res.ok) return;
|
|
12322
12417
|
const data = await res.json();
|
|
12323
12418
|
const serverMsgs = Array.isArray(data?.messages) ? data.messages : [];
|
|
12324
|
-
mergeAgentMessages(serverMsgs.filter(
|
|
12419
|
+
mergeAgentMessages(serverMsgs.filter(isRepliedOutsideStream));
|
|
12325
12420
|
} catch {
|
|
12326
12421
|
/* network/parse failure - the live stream still carries new messages */
|
|
12327
12422
|
}
|
|
@@ -12358,6 +12453,8 @@
|
|
|
12358
12453
|
// Advance the backfill watermark so a later reconnect doesn't
|
|
12359
12454
|
// re-request everything we have already rendered live.
|
|
12360
12455
|
noteAgentMessageAt(data.message.createdAt);
|
|
12456
|
+
const owner = responderAfterPush(data.message);
|
|
12457
|
+
if (owner === 'ai') markAiOwned();else if (owner === 'human') markHandedOff();
|
|
12361
12458
|
setMessages(prev => {
|
|
12362
12459
|
if (prev.some(m => m.id === data.message.id)) return prev;
|
|
12363
12460
|
return [...prev, createMessage({
|
|
@@ -12386,7 +12483,7 @@
|
|
|
12386
12483
|
// would disable that recovery.
|
|
12387
12484
|
es.onerror = () => {};
|
|
12388
12485
|
handoffEventSourceRef.current = es;
|
|
12389
|
-
}, [apiBaseUrl, bidiSessionId, getAuthToken, backfillAgentMessages]);
|
|
12486
|
+
}, [apiBaseUrl, bidiSessionId, getAuthToken, backfillAgentMessages, markAiOwned, markHandedOff]);
|
|
12390
12487
|
reactExports.useEffect(() => {
|
|
12391
12488
|
return () => {
|
|
12392
12489
|
if (handoffEventSourceRef.current) {
|
|
@@ -12635,7 +12732,7 @@
|
|
|
12635
12732
|
if (!res.ok || cancelled) return;
|
|
12636
12733
|
const data = await res.json();
|
|
12637
12734
|
const serverMsgs = Array.isArray(data?.messages) ? data.messages : [];
|
|
12638
|
-
const agentMsgs = serverMsgs.filter(
|
|
12735
|
+
const agentMsgs = serverMsgs.filter(isRepliedOutsideStream);
|
|
12639
12736
|
|
|
12640
12737
|
// Merge past human-agent replies into the transcript (regardless of
|
|
12641
12738
|
// current responder - they're real history worth showing). Shares
|
|
@@ -13902,6 +13999,9 @@
|
|
|
13902
13999
|
// Reset userHasSpoken flag when stopping session
|
|
13903
14000
|
userHasSpokenRef.current = false;
|
|
13904
14001
|
pendingVoiceSourcesRef.current = null;
|
|
14002
|
+
if (bargeInTimerRef.current) clearTimeout(bargeInTimerRef.current);
|
|
14003
|
+
bargeInTimerRef.current = null;
|
|
14004
|
+
bargeInPendingRef.current = false;
|
|
13905
14005
|
|
|
13906
14006
|
// Cancel a pending reconnect, drop the ref FIRST (so this socket's own
|
|
13907
14007
|
// onclose sees it is no longer current), then close it in any state.
|
|
@@ -13958,6 +14058,11 @@
|
|
|
13958
14058
|
setVoiceError("voiceUnavailable");
|
|
13959
14059
|
}
|
|
13960
14060
|
|
|
14061
|
+
// The backend's verdict on the sound that paused the reply: play on, or
|
|
14062
|
+
// drop what is left of it (lib/audio/bargeIn.js).
|
|
14063
|
+
const bargeIn = bargeInVerdict(event);
|
|
14064
|
+
if (bargeIn) settleBargeIn(bargeIn);
|
|
14065
|
+
|
|
13961
14066
|
// Handle conversation conclusion for voice (bidi) sessions
|
|
13962
14067
|
const conclusionDetected = event.conclusion_detected ?? event.conclusionDetected;
|
|
13963
14068
|
if (typeof conclusionDetected === "boolean" && conclusionDetected) {
|
|
@@ -14152,7 +14257,7 @@
|
|
|
14152
14257
|
if (!event.interrupted && !event.inputTranscription && !event.outputTranscription && !event.content && !event.prompts) {
|
|
14153
14258
|
console.warn("[BIDI] handleBidiEvent: event had no recognized fields", event);
|
|
14154
14259
|
}
|
|
14155
|
-
}, [appendAssistantContent, appendUserTranscription, handleContentParts, handleInterrupted, handleTurnComplete, stopVoiceSession]);
|
|
14260
|
+
}, [appendAssistantContent, appendUserTranscription, handleContentParts, handleInterrupted, handleTurnComplete, stopVoiceSession, settleBargeIn]);
|
|
14156
14261
|
const handleRecorderAudio = reactExports.useCallback(pcmData => {
|
|
14157
14262
|
if (!isVoiceSessionActiveRef.current) {
|
|
14158
14263
|
return;
|
|
@@ -14194,17 +14299,18 @@
|
|
|
14194
14299
|
speechGateRef.current = createSpeechGate({
|
|
14195
14300
|
sampleRate: 16000
|
|
14196
14301
|
}, {
|
|
14197
|
-
// Barge-in: the instant the user
|
|
14198
|
-
// TTS still playing so we don't talk over them
|
|
14199
|
-
//
|
|
14200
|
-
//
|
|
14302
|
+
// Barge-in: the instant the user may be talking, pause any assistant
|
|
14303
|
+
// TTS still playing so we don't talk over them, and tell the backend
|
|
14304
|
+
// to hold the in-flight reply. The backend's verdict on what it heard
|
|
14305
|
+
// then resumes the audio or drops it (lib/audio/bargeIn.js): emptying
|
|
14306
|
+
// the player here lost the rest of a reply to a cough (dev, 16 Oct).
|
|
14201
14307
|
onSpeechStart: () => {
|
|
14202
|
-
// Barge-in: user started talking → (1) stop assistant TTS already
|
|
14203
|
-
// buffered locally, and (2) tell the backend to cancel the in-flight
|
|
14204
|
-
// reply so its remaining TTS doesn't clobber the next turn's audio.
|
|
14205
14308
|
audioPlayerNodeRef.current?.port.postMessage({
|
|
14206
|
-
command: "
|
|
14309
|
+
command: "pause"
|
|
14207
14310
|
});
|
|
14311
|
+
bargeInPendingRef.current = true;
|
|
14312
|
+
if (bargeInTimerRef.current) clearTimeout(bargeInTimerRef.current);
|
|
14313
|
+
bargeInTimerRef.current = null;
|
|
14208
14314
|
const ws = websocketRef.current;
|
|
14209
14315
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
14210
14316
|
ws.send(JSON.stringify({
|
|
@@ -14220,6 +14326,11 @@
|
|
|
14220
14326
|
type: "audio_end"
|
|
14221
14327
|
}));
|
|
14222
14328
|
}
|
|
14329
|
+
// no verdict on the sound: the reply plays on
|
|
14330
|
+
if (bargeInPendingRef.current) {
|
|
14331
|
+
if (bargeInTimerRef.current) clearTimeout(bargeInTimerRef.current);
|
|
14332
|
+
bargeInTimerRef.current = setTimeout(() => settleBargeIn("resume"), BARGE_IN_RESUME_AFTER_MS);
|
|
14333
|
+
}
|
|
14223
14334
|
}
|
|
14224
14335
|
});
|
|
14225
14336
|
const [recorderNode, recorderContext, micStream] = await startAudioRecorderWorklet(handleRecorderAudio);
|
|
@@ -14232,7 +14343,7 @@
|
|
|
14232
14343
|
console.error("Failed to initialize audio pipeline", error);
|
|
14233
14344
|
throw error;
|
|
14234
14345
|
}
|
|
14235
|
-
}, [handleRecorderAudio]);
|
|
14346
|
+
}, [handleRecorderAudio, settleBargeIn]);
|
|
14236
14347
|
const connectWebsocket = reactExports.useCallback(async () => {
|
|
14237
14348
|
if (typeof window === "undefined" || !websocketUrl) {
|
|
14238
14349
|
return;
|
|
@@ -16889,6 +17000,10 @@
|
|
|
16889
17000
|
isRtl = false,
|
|
16890
17001
|
questionTextColor,
|
|
16891
17002
|
questionBoxColor,
|
|
17003
|
+
// User bubble colours, same props ChatInput's follow-up chips take.
|
|
17004
|
+
resolvedUserMessageColor,
|
|
17005
|
+
userMessageBoxColor,
|
|
17006
|
+
userTextColor,
|
|
16892
17007
|
// Optional localized heading rendered above the vertical pill list,
|
|
16893
17008
|
// styled to match the inline category picker's helper-text heading.
|
|
16894
17009
|
headingLabel
|
|
@@ -16918,9 +17033,14 @@
|
|
|
16918
17033
|
});
|
|
16919
17034
|
}
|
|
16920
17035
|
|
|
16921
|
-
// Vertical pills
|
|
16922
|
-
// fallback
|
|
16923
|
-
|
|
17036
|
+
// Vertical pills take their fill and text colour from the user bubble, with
|
|
17037
|
+
// the same fallback chains as the follow-up chips (ChatInput's
|
|
17038
|
+
// PromptSuggestionsRow): a click sends the question as the user's own
|
|
17039
|
+
// message. The agent bubble's text colour sat on a white pill, so a white
|
|
17040
|
+
// assistant text colour made the questions vanish. No border, like the
|
|
17041
|
+
// follow-up chips.
|
|
17042
|
+
const chipBackground = resolvedUserMessageColor ?? userMessageBoxColor ?? primaryColor ?? "#2563eb";
|
|
17043
|
+
const chipTextColor = userTextColor ?? textColor ?? "#FFFFFF";
|
|
16924
17044
|
|
|
16925
17045
|
// Vertical layout (default) - pill style matching chat-preview
|
|
16926
17046
|
return /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
|
|
@@ -16947,8 +17067,8 @@
|
|
|
16947
17067
|
fontFamily,
|
|
16948
17068
|
fontSize,
|
|
16949
17069
|
color: chipTextColor,
|
|
16950
|
-
backgroundColor:
|
|
16951
|
-
border:
|
|
17070
|
+
backgroundColor: chipBackground,
|
|
17071
|
+
border: "none",
|
|
16952
17072
|
borderRadius: "9999px",
|
|
16953
17073
|
padding: "5px 14px",
|
|
16954
17074
|
fontWeight: 500,
|
|
@@ -16956,12 +17076,6 @@
|
|
|
16956
17076
|
whiteSpace: "normal",
|
|
16957
17077
|
textAlign: questionIsRtl ? "right" : "left"
|
|
16958
17078
|
},
|
|
16959
|
-
onMouseEnter: e => {
|
|
16960
|
-
e.currentTarget.style.backgroundColor = `${primaryColor}14`;
|
|
16961
|
-
},
|
|
16962
|
-
onMouseLeave: e => {
|
|
16963
|
-
e.currentTarget.style.backgroundColor = isDark ? "transparent" : "#ffffff";
|
|
16964
|
-
},
|
|
16965
17079
|
children: question
|
|
16966
17080
|
}, index);
|
|
16967
17081
|
})
|
|
@@ -17763,6 +17877,9 @@
|
|
|
17763
17877
|
isRtl: isRtl,
|
|
17764
17878
|
questionTextColor: assistantTextColor,
|
|
17765
17879
|
questionBoxColor: agentMessageBubbleColor,
|
|
17880
|
+
resolvedUserMessageColor: resolvedUserMessageColor,
|
|
17881
|
+
userMessageBoxColor: userMessageBoxColor,
|
|
17882
|
+
userTextColor: userTextColor,
|
|
17766
17883
|
headingLabel: quickQuestionsHeadingLabel
|
|
17767
17884
|
})
|
|
17768
17885
|
}), isVoiceSessionActive && voiceStatus === "connected" && !isLoading && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
|
|
@@ -21656,7 +21773,7 @@
|
|
|
21656
21773
|
* value by its own test (that constant sat at 0.1.113 while this package was
|
|
21657
21774
|
* at 0.2.2 precisely because nothing enforced it - EV-2431).
|
|
21658
21775
|
*/
|
|
21659
|
-
const WIDGET_VERSION = '0.2.
|
|
21776
|
+
const WIDGET_VERSION = '0.2.19';
|
|
21660
21777
|
|
|
21661
21778
|
/**
|
|
21662
21779
|
* API utility functions
|