@eshal-bot/chat-widget 0.2.7 → 0.2.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.
- package/dist/{chat-widget-MarkdownRenderer-DNrjrl0W.esm.js → chat-widget-MarkdownRenderer-BzYOMvut.esm.js} +2 -2
- package/dist/{chat-widget-index-8wTGWK67.esm.js → chat-widget-index-CUDFhUWj.esm.js} +2 -2
- package/dist/chat-widget-markdown-chunk.js +9 -4
- package/dist/chat-widget-markdown-chunk.min.js +1 -1
- package/dist/chat-widget.esm.js +1 -1
- package/dist/chat-widget.js +105 -17
- package/dist/chat-widget.min.js +2 -2
- package/dist/chat-widget.umd.js +3 -3
- package/package.json +1 -1
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-CUDFhUWj.esm.js";import"react";import"react-dom/client";import"react/jsx-runtime";import"react-dom";
|
package/dist/chat-widget.js
CHANGED
|
@@ -8372,7 +8372,7 @@
|
|
|
8372
8372
|
stale: s.stale
|
|
8373
8373
|
})).filter(s => s.source_id || s.url || s.source_type === 'database') : [];
|
|
8374
8374
|
};
|
|
8375
|
-
const processStream = async (stream, setMessages) => {
|
|
8375
|
+
const processStream = async (stream, setMessages, onTerminal) => {
|
|
8376
8376
|
const updateMessage = createMessageUpdater(setMessages);
|
|
8377
8377
|
const reader = stream.getReader();
|
|
8378
8378
|
const decoder = new TextDecoder();
|
|
@@ -8388,6 +8388,35 @@
|
|
|
8388
8388
|
// closes without one, the connection dropped mid-response and we
|
|
8389
8389
|
// throw IncompleteStreamError so the caller can retry.
|
|
8390
8390
|
let streamCompleted = false;
|
|
8391
|
+
// The id of THIS turn's assistant bubble, captured when the terminal
|
|
8392
|
+
// event finalizes it. Used to address the trailing chips chunk below:
|
|
8393
|
+
// once the composer unlocks at `complete`, "the last message" may
|
|
8394
|
+
// already belong to a NEWER turn.
|
|
8395
|
+
let turnAssistantId = null;
|
|
8396
|
+
|
|
8397
|
+
// EV-2872 completion: the backend emits `complete` WITHOUT waiting for
|
|
8398
|
+
// chip generation, precisely so the composer can unlock at last-token
|
|
8399
|
+
// time - then keeps the stream open for the trailing
|
|
8400
|
+
// {type:'follow_up_prompts'} chunk and [DONE]. Measured on dev
|
|
8401
|
+
// 2026-09-18, that tail runs 5.9-7.5s (budgeted up to 15s by
|
|
8402
|
+
// RAG_AUTO_PROMPT_TIMEOUT_S), and the widget spent all of it with the
|
|
8403
|
+
// composer disabled because the caller only cleared isLoading when the
|
|
8404
|
+
// HTTP body closed. Firing here hands the caller the real unlock point
|
|
8405
|
+
// while the reader keeps draining the chips in the background. Fires
|
|
8406
|
+
// at most ONCE per stream, from whichever terminal event arrives first.
|
|
8407
|
+
const markTerminal = () => {
|
|
8408
|
+
if (streamCompleted) return;
|
|
8409
|
+
streamCompleted = true;
|
|
8410
|
+
if (typeof onTerminal === 'function') {
|
|
8411
|
+
try {
|
|
8412
|
+
onTerminal();
|
|
8413
|
+
} catch (cbErr) {
|
|
8414
|
+
// A caller-side throw must never turn a delivered answer into a
|
|
8415
|
+
// dropped stream (the catch below would surface it as a retry).
|
|
8416
|
+
console.warn('[chat] onTerminal callback threw:', cbErr);
|
|
8417
|
+
}
|
|
8418
|
+
}
|
|
8419
|
+
};
|
|
8391
8420
|
try {
|
|
8392
8421
|
while (true) {
|
|
8393
8422
|
const {
|
|
@@ -8460,7 +8489,7 @@
|
|
|
8460
8489
|
// would retry-send the message 3× and render an error bubble). The
|
|
8461
8490
|
// human agent's reply arrives via the SSE EventSource instead.
|
|
8462
8491
|
if (parsed.type === 'human_handoff') {
|
|
8463
|
-
|
|
8492
|
+
markTerminal();
|
|
8464
8493
|
continue;
|
|
8465
8494
|
}
|
|
8466
8495
|
|
|
@@ -8474,7 +8503,7 @@
|
|
|
8474
8503
|
// just get suppressed again). Mirrors packages/frontend/src/
|
|
8475
8504
|
// services/ChatService.ts's handling of this event.
|
|
8476
8505
|
if (parsed.type === 'duplicate_suppressed') {
|
|
8477
|
-
|
|
8506
|
+
markTerminal();
|
|
8478
8507
|
continue;
|
|
8479
8508
|
}
|
|
8480
8509
|
|
|
@@ -8494,14 +8523,25 @@
|
|
|
8494
8523
|
setMessages(prev => {
|
|
8495
8524
|
const updated = [...(prev || [])];
|
|
8496
8525
|
const lastMsg = updated[updated.length - 1];
|
|
8497
|
-
if (lastMsg
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8526
|
+
if (!lastMsg || lastMsg.role !== 'assistant') return prev;
|
|
8527
|
+
// The composer now unlocks at `complete`, so by the time this
|
|
8528
|
+
// chunk lands the user may already have asked the NEXT
|
|
8529
|
+
// question - in which case the last message belongs to that
|
|
8530
|
+
// turn, and writing our chips onto it would caption a new
|
|
8531
|
+
// answer with the old one's follow-ups. Worse, sendMessage
|
|
8532
|
+
// deliberately clears prior chips on every send (EV-3080:
|
|
8533
|
+
// stale chips must never look current), so this write would
|
|
8534
|
+
// undo that. Land the chips only while this turn's bubble is
|
|
8535
|
+
// still the one in front of the user; otherwise drop them.
|
|
8536
|
+
if (turnAssistantId != null && lastMsg.id !== turnAssistantId) {
|
|
8537
|
+
return prev;
|
|
8504
8538
|
}
|
|
8539
|
+
// Same storage shape updateFinalMessage uses on the
|
|
8540
|
+
// complete chunk: message.prompts, normalized.
|
|
8541
|
+
updated[updated.length - 1] = {
|
|
8542
|
+
...lastMsg,
|
|
8543
|
+
prompts: normalizePrompts(latePrompts)
|
|
8544
|
+
};
|
|
8505
8545
|
return updated;
|
|
8506
8546
|
});
|
|
8507
8547
|
}
|
|
@@ -8570,7 +8610,6 @@
|
|
|
8570
8610
|
|
|
8571
8611
|
// Handle complete events
|
|
8572
8612
|
if (parsed.type === 'complete') {
|
|
8573
|
-
streamCompleted = true;
|
|
8574
8613
|
// Normalize Sources from stream. citation_indices are
|
|
8575
8614
|
// preserved so <CitationPill> resolves [N] markers past the
|
|
8576
8615
|
// inline-cite filter; database "Data used" entries keep their
|
|
@@ -8614,6 +8653,13 @@
|
|
|
8614
8653
|
setMessages(prev => {
|
|
8615
8654
|
const updatedMessages = [...(prev || [])];
|
|
8616
8655
|
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
|
8656
|
+
if (lastMessage?.role === 'assistant') {
|
|
8657
|
+
// Remember which bubble this turn owns, before the composer
|
|
8658
|
+
// unlocks below and a newer turn can append past it. The
|
|
8659
|
+
// assignment is idempotent (same id every call), so a
|
|
8660
|
+
// StrictMode double-invoke is harmless.
|
|
8661
|
+
turnAssistantId = lastMessage.id ?? null;
|
|
8662
|
+
}
|
|
8617
8663
|
if (lastMessage?.role === 'assistant' && (sources.length > 0 || lastMessage.sourcesPreliminary)) {
|
|
8618
8664
|
const {
|
|
8619
8665
|
sourcesPreliminary,
|
|
@@ -8629,6 +8675,11 @@
|
|
|
8629
8675
|
}
|
|
8630
8676
|
return updatedMessages;
|
|
8631
8677
|
});
|
|
8678
|
+
|
|
8679
|
+
// Answer + citations are committed - release the composer now
|
|
8680
|
+
// rather than after the trailing chips chunk and [DONE]. The
|
|
8681
|
+
// reader keeps draining below.
|
|
8682
|
+
markTerminal();
|
|
8632
8683
|
continue;
|
|
8633
8684
|
}
|
|
8634
8685
|
|
|
@@ -8679,11 +8730,19 @@
|
|
|
8679
8730
|
|
|
8680
8731
|
// Handle finish_reason (stream complete)
|
|
8681
8732
|
if (parsed.choices?.[0]?.finish_reason && parsed.choices[0].finish_reason !== null) {
|
|
8682
|
-
streamCompleted = true;
|
|
8683
8733
|
const metadata = {
|
|
8684
8734
|
prompts: parsed.prompts
|
|
8685
8735
|
};
|
|
8686
8736
|
updateFinalMessage(setMessages, accumulatedText.current, metadata);
|
|
8737
|
+
// Same bubble capture the `complete` branch does, so this terminal
|
|
8738
|
+
// shape gets the same late-chip targeting. Read-only: returning
|
|
8739
|
+
// `prev` unchanged makes React bail out of the re-render.
|
|
8740
|
+
setMessages(prev => {
|
|
8741
|
+
const last = (prev || [])[(prev || []).length - 1];
|
|
8742
|
+
if (last?.role === 'assistant') turnAssistantId = last.id ?? null;
|
|
8743
|
+
return prev;
|
|
8744
|
+
});
|
|
8745
|
+
markTerminal();
|
|
8687
8746
|
}
|
|
8688
8747
|
}
|
|
8689
8748
|
}
|
|
@@ -12041,6 +12100,15 @@
|
|
|
12041
12100
|
// call before any state mutation runs.
|
|
12042
12101
|
const isSendingRef = reactExports.useRef(false);
|
|
12043
12102
|
|
|
12103
|
+
// Monotonic id for each sendMessage call. Since the composer now unlocks at
|
|
12104
|
+
// the stream's `complete` event rather than when the HTTP body closes, turn
|
|
12105
|
+
// N's stream is still draining its trailing chips chunk while turn N+1 may
|
|
12106
|
+
// already be in flight - so turn N's `finally` would otherwise clear the
|
|
12107
|
+
// loading state that belongs to turn N+1 and unlock the composer mid-answer.
|
|
12108
|
+
// Every release is stamped with the send it came from and ignored when a
|
|
12109
|
+
// newer send has taken over.
|
|
12110
|
+
const sendSeqRef = reactExports.useRef(0);
|
|
12111
|
+
|
|
12044
12112
|
// Bidi/Voice state
|
|
12045
12113
|
const [isConversationLoading, setIsConversationLoading] = reactExports.useState(false);
|
|
12046
12114
|
const [isVoiceSessionActive, setIsVoiceSessionActive] = reactExports.useState(false);
|
|
@@ -12603,6 +12671,13 @@
|
|
|
12603
12671
|
};
|
|
12604
12672
|
}
|
|
12605
12673
|
isSendingRef.current = true;
|
|
12674
|
+
const mySendSeq = sendSeqRef.current += 1;
|
|
12675
|
+
// Idempotent, and a no-op once a newer send owns the composer.
|
|
12676
|
+
const releaseComposer = () => {
|
|
12677
|
+
if (sendSeqRef.current !== mySendSeq) return;
|
|
12678
|
+
setIsLoading(false);
|
|
12679
|
+
isSendingRef.current = false;
|
|
12680
|
+
};
|
|
12606
12681
|
if (trimmed === HANDOFF_TRIGGER) {
|
|
12607
12682
|
const fireWidgetHandoff = async () => {
|
|
12608
12683
|
if (apiBaseUrl && bidiSessionId) {
|
|
@@ -12877,7 +12952,15 @@
|
|
|
12877
12952
|
// exactly the attempt that is streaming.
|
|
12878
12953
|
streamAbortRef.current = new AbortController();
|
|
12879
12954
|
const stream = await sendStreamingRequest(chatRequest, apiBaseUrl, getAuthToken, streamAbortRef.current.signal);
|
|
12880
|
-
|
|
12955
|
+
// EV-2872 completion: unlock the composer on the stream's
|
|
12956
|
+
// terminal event (`complete`), not when the body closes. The
|
|
12957
|
+
// backend holds the stream open after `complete` for the
|
|
12958
|
+
// trailing suggestion-chip chunk - measured at 5.9-7.5s on dev
|
|
12959
|
+
// 2026-09-18, budgeted up to 15s - and the customer spent all of
|
|
12960
|
+
// it unable to type their next question. Retries are unaffected:
|
|
12961
|
+
// IncompleteStreamError is only thrown when NO terminal event
|
|
12962
|
+
// arrived, so nothing that reaches this callback is ever retried.
|
|
12963
|
+
await processStream(stream, setMessages, releaseComposer);
|
|
12881
12964
|
setUserMessageStatus(sentUserMessageId, "sent");
|
|
12882
12965
|
updateActivity(organizationId);
|
|
12883
12966
|
return {
|
|
@@ -12956,9 +13039,14 @@
|
|
|
12956
13039
|
// input until the next regular message ran. setIsLoading(false) is
|
|
12957
13040
|
// a no-op when it's already false, so widening the condition can't
|
|
12958
13041
|
// regress the inquiry/complaint/technical path.
|
|
12959
|
-
|
|
12960
|
-
//
|
|
12961
|
-
|
|
13042
|
+
//
|
|
13043
|
+
// Still the ONLY release for every non-streaming path (onboarding,
|
|
13044
|
+
// handoff, the error branches above) and the backstop for a stream
|
|
13045
|
+
// that ends without a terminal event. Streaming turns have usually
|
|
13046
|
+
// released already, at `complete`; this call is then a no-op. It is
|
|
13047
|
+
// sequence-guarded so a turn whose chips are still draining cannot
|
|
13048
|
+
// unlock the composer out from under the turn that followed it.
|
|
13049
|
+
releaseComposer();
|
|
12962
13050
|
}
|
|
12963
13051
|
}, [apiBaseUrl, apiKey, getNextMessageId, getAuthToken, messages, bidiSessionId, routingChoice, widgetFilter, provideCitations]);
|
|
12964
13052
|
|
|
@@ -20893,7 +20981,7 @@
|
|
|
20893
20981
|
* value by its own test (that constant sat at 0.1.113 while this package was
|
|
20894
20982
|
* at 0.2.2 precisely because nothing enforced it - EV-2431).
|
|
20895
20983
|
*/
|
|
20896
|
-
const WIDGET_VERSION = '0.2.
|
|
20984
|
+
const WIDGET_VERSION = '0.2.9';
|
|
20897
20985
|
|
|
20898
20986
|
/**
|
|
20899
20987
|
* API utility functions
|