@eshal-bot/chat-widget 0.2.12 → 0.2.14
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-CBSXpNd6.esm.js → chat-widget-MarkdownRenderer-Dj2CxQVq.esm.js} +6 -6
- package/dist/chat-widget-index-DnppNZQ8.esm.js +20 -0
- package/dist/chat-widget-markdown-chunk.js +122 -10
- package/dist/chat-widget-markdown-chunk.min.js +1 -1
- package/dist/chat-widget.esm.js +1 -1
- package/dist/chat-widget.js +492 -106
- package/dist/chat-widget.min.js +4 -4
- package/dist/chat-widget.umd.js +8 -8
- package/dist/index.d.ts +6 -0
- package/index.d.ts +6 -0
- package/package.json +1 -1
- package/dist/chat-widget-index-DqpnMEPr.esm.js +0 -20
|
@@ -88402,6 +88402,26 @@
|
|
|
88402
88402
|
return typeof u === 'string' && /^https?:\/\//i.test(u) ? u : undefined;
|
|
88403
88403
|
}
|
|
88404
88404
|
|
|
88405
|
+
// Kept in its own tiny module (not A2uiBlock.jsx): MessageList imports it
|
|
88406
|
+
// eagerly, and A2uiBlock/MarkdownRenderer are lazy-loaded to keep charts and
|
|
88407
|
+
// react-markdown out of the widget's initial bundle.
|
|
88408
|
+
|
|
88409
|
+
// 2026-09-26 (P1, double-booking risk): the delivered user turn that
|
|
88410
|
+
// followed the message carrying a card, derived from conversation history by
|
|
88411
|
+
// MessageList. `undefined` = not answered yet. One-shot cards (confirmation,
|
|
88412
|
+
// form) render locked once answered, so a reload cannot re-arm an answered
|
|
88413
|
+
// "Confirm booking". Context rather than a prop so it does not have to be
|
|
88414
|
+
// threaded through MessageBubble -> StreamingMarkdown -> MarkdownRenderer.
|
|
88415
|
+
const A2uiAnsweredContext = /*#__PURE__*/React.createContext(undefined);
|
|
88416
|
+
const _normAnswer = v => String(v ?? '').trim().toLowerCase();
|
|
88417
|
+
function matchAnsweredChoice(choices, answer) {
|
|
88418
|
+
if (answer === undefined || answer === null) return null;
|
|
88419
|
+
const a = _normAnswer(answer);
|
|
88420
|
+
if (!a) return null;
|
|
88421
|
+
const hit = (choices || []).find(c => _normAnswer(c?.value) === a || _normAnswer(c?.label) === a);
|
|
88422
|
+
return hit ? String(hit.value ?? hit.label ?? '') : null;
|
|
88423
|
+
}
|
|
88424
|
+
|
|
88405
88425
|
/**
|
|
88406
88426
|
* A2uiBlock - render `eshal-ui` / `eshal-chart` / `chart` fence JSON as a
|
|
88407
88427
|
* real visual block inside the embedded chat widget.
|
|
@@ -89343,6 +89363,9 @@
|
|
|
89343
89363
|
const [values, setValues] = React.useState(initialValues);
|
|
89344
89364
|
const [errors, setErrors] = React.useState({});
|
|
89345
89365
|
const [submitted, setSubmitted] = React.useState(false);
|
|
89366
|
+
// A later user turn already answered this form (history), e.g. after a
|
|
89367
|
+
// reload: never re-arm the submit.
|
|
89368
|
+
const consumed = React.useContext(A2uiAnsweredContext) !== undefined;
|
|
89346
89369
|
if (fields.length === 0) return null;
|
|
89347
89370
|
const handleChange = (name, value) => {
|
|
89348
89371
|
setValues(prev => ({
|
|
@@ -89356,7 +89379,7 @@
|
|
|
89356
89379
|
};
|
|
89357
89380
|
const handleSubmit = e => {
|
|
89358
89381
|
e.preventDefault();
|
|
89359
|
-
if (submitted || !onButtonClick) return;
|
|
89382
|
+
if (submitted || consumed || !onButtonClick) return;
|
|
89360
89383
|
const next = {};
|
|
89361
89384
|
let firstError = false;
|
|
89362
89385
|
for (const f of fields) {
|
|
@@ -89440,17 +89463,17 @@
|
|
|
89440
89463
|
autoId: `a2ui-form-${f.name || i}`
|
|
89441
89464
|
}, f.name || i)), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
|
|
89442
89465
|
type: "submit",
|
|
89443
|
-
disabled: !onButtonClick,
|
|
89466
|
+
disabled: !onButtonClick || consumed,
|
|
89444
89467
|
style: {
|
|
89445
89468
|
border: '1px solid ' + (isDark ? '#374151' : '#d1d5db'),
|
|
89446
|
-
background: onButtonClick ? '#2563eb' : isDark ? '#1f2937' : '#f3f4f6',
|
|
89447
|
-
color: onButtonClick ? '#ffffff' : isDark ? '#6b7280' : '#9ca3af',
|
|
89469
|
+
background: onButtonClick && !consumed ? '#2563eb' : isDark ? '#1f2937' : '#f3f4f6',
|
|
89470
|
+
color: onButtonClick && !consumed ? '#ffffff' : isDark ? '#6b7280' : '#9ca3af',
|
|
89448
89471
|
padding: '6px 14px',
|
|
89449
89472
|
borderRadius: 6,
|
|
89450
89473
|
fontSize: 12,
|
|
89451
|
-
cursor: onButtonClick ? 'pointer' : 'default'
|
|
89474
|
+
cursor: onButtonClick && !consumed ? 'pointer' : 'default'
|
|
89452
89475
|
},
|
|
89453
|
-
children: spec.submitLabel || 'Submit'
|
|
89476
|
+
children: consumed ? 'Submitted' : spec.submitLabel || 'Submit'
|
|
89454
89477
|
})]
|
|
89455
89478
|
})
|
|
89456
89479
|
);
|
|
@@ -89474,6 +89497,26 @@
|
|
|
89474
89497
|
label: 'No',
|
|
89475
89498
|
value: 'no'
|
|
89476
89499
|
}];
|
|
89500
|
+
// 2026-09-26 (P1): a confirmation is a one-shot commitment. It used to
|
|
89501
|
+
// stay clickable forever - after the pick AND after a reload - so a second
|
|
89502
|
+
// click re-sent "confirm" (double-booking risk). Lock on the local pick
|
|
89503
|
+
// (ref guard: a double click in one tick cannot send twice) or on the
|
|
89504
|
+
// history-recorded answer, and tick the chosen option.
|
|
89505
|
+
const consumedBy = React.useContext(A2uiAnsweredContext);
|
|
89506
|
+
const [localChosen, setLocalChosen] = React.useState(null);
|
|
89507
|
+
const sentRef = React.useRef(false);
|
|
89508
|
+
const chosen = localChosen ?? matchAnsweredChoice(buttons, consumedBy);
|
|
89509
|
+
const locked = chosen !== null || consumedBy !== undefined;
|
|
89510
|
+
const lockedButtons = locked ? buttons.map(b => String(b?.value ?? b?.label ?? '') === chosen ? {
|
|
89511
|
+
...b,
|
|
89512
|
+
label: `${String(b.label ?? b.value ?? '')} \u2713`
|
|
89513
|
+
} : b) : buttons;
|
|
89514
|
+
const handleChoose = onButtonClick ? (value, label) => {
|
|
89515
|
+
if (sentRef.current || locked) return;
|
|
89516
|
+
sentRef.current = true;
|
|
89517
|
+
setLocalChosen(String(value));
|
|
89518
|
+
onButtonClick(value, label);
|
|
89519
|
+
} : undefined;
|
|
89477
89520
|
return /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
|
|
89478
89521
|
style: card(isDark),
|
|
89479
89522
|
children: [spec.title && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
|
|
@@ -89493,10 +89536,10 @@
|
|
|
89493
89536
|
children: String(spec.body)
|
|
89494
89537
|
}), /*#__PURE__*/jsxRuntimeExports.jsx(ButtonGroup, {
|
|
89495
89538
|
spec: {
|
|
89496
|
-
buttons
|
|
89539
|
+
buttons: lockedButtons
|
|
89497
89540
|
},
|
|
89498
89541
|
isDark: isDark,
|
|
89499
|
-
onButtonClick:
|
|
89542
|
+
onButtonClick: locked ? undefined : handleChoose
|
|
89500
89543
|
})]
|
|
89501
89544
|
});
|
|
89502
89545
|
}
|
|
@@ -91573,6 +91616,70 @@
|
|
|
91573
91616
|
const globalScope = typeof window !== "undefined" ? window : typeof globalThis !== "undefined" ? globalThis : undefined;
|
|
91574
91617
|
const WidgetCitationContext = globalScope ? globalScope[GLOBAL_KEY] || (globalScope[GLOBAL_KEY] = /*#__PURE__*/React.createContext(null)) : /*#__PURE__*/React.createContext(null);
|
|
91575
91618
|
|
|
91619
|
+
/**
|
|
91620
|
+
* EV-4177 - can this source card actually be downloaded?
|
|
91621
|
+
*
|
|
91622
|
+
* The chat showed a download icon on every `source_type: "file"` card and then
|
|
91623
|
+
* did nothing when it was clicked. QA uploaded books.xlsx, asked about it, got
|
|
91624
|
+
* an answer citing the file, clicked the icon, and nothing happened - no
|
|
91625
|
+
* download, no error, no explanation.
|
|
91626
|
+
*
|
|
91627
|
+
* The download handler has always had preconditions. An attachment card needs
|
|
91628
|
+
* the presigned read URL the backend mints (`source.url`); a SharePoint card
|
|
91629
|
+
* needs its proxy URL; a knowledge-base card needs a `source_id` the
|
|
91630
|
+
* `/file-download` endpoint can resolve. When none of those are present the
|
|
91631
|
+
* handler logged a warning to a console nobody has open and returned.
|
|
91632
|
+
*
|
|
91633
|
+
* The bug is not really the failed download - it is the icon. Offering a
|
|
91634
|
+
* control that cannot work is worse than not offering it: the customer clicks
|
|
91635
|
+
* it repeatedly, assumes the product is broken, and has nothing to report
|
|
91636
|
+
* beyond "it doesn't work".
|
|
91637
|
+
*
|
|
91638
|
+
* So the same conditions the handler checks now decide whether the icon is
|
|
91639
|
+
* drawn at all, in one place used by both the message bubble and the citation
|
|
91640
|
+
* pill. Those two had grown separate copies of "is this a file", which is how
|
|
91641
|
+
* they came to disagree about what is downloadable.
|
|
91642
|
+
*/
|
|
91643
|
+
|
|
91644
|
+
/** A SharePoint citation is served through our own proxy endpoint. */
|
|
91645
|
+
function isSharePointProxyUrl(url) {
|
|
91646
|
+
return typeof url === "string" && url.includes("/api/v1/sharepoint/file/");
|
|
91647
|
+
}
|
|
91648
|
+
|
|
91649
|
+
/**
|
|
91650
|
+
* Uploaded chat attachments are keyed `attachment:<storage_key>`, which the
|
|
91651
|
+
* legacy `/file-download` endpoint cannot resolve - they are fetched straight
|
|
91652
|
+
* from the presigned URL instead, so that URL is mandatory for them.
|
|
91653
|
+
*/
|
|
91654
|
+
function isAttachmentSource(source) {
|
|
91655
|
+
if (!source) return false;
|
|
91656
|
+
const id = String(source.source_id || source.sourceid || "");
|
|
91657
|
+
return id.startsWith("attachment:");
|
|
91658
|
+
}
|
|
91659
|
+
|
|
91660
|
+
/**
|
|
91661
|
+
* True only when a click will actually produce a file.
|
|
91662
|
+
*
|
|
91663
|
+
* Deliberately mirrors the branches in the download handler. If you add a new
|
|
91664
|
+
* download path there, add it here too - the icon and the action have to agree,
|
|
91665
|
+
* and this function existing is what makes that a single edit rather than a
|
|
91666
|
+
* bug report.
|
|
91667
|
+
*/
|
|
91668
|
+
function canDownloadSource(source) {
|
|
91669
|
+
if (!source) return false;
|
|
91670
|
+
|
|
91671
|
+
// Attachment cards: the presigned URL is the only way in.
|
|
91672
|
+
if (isAttachmentSource(source)) {
|
|
91673
|
+
return typeof source.url === "string" && source.url.length > 0;
|
|
91674
|
+
}
|
|
91675
|
+
|
|
91676
|
+
// SharePoint: the proxy URL is the only way in.
|
|
91677
|
+
if (isSharePointProxyUrl(source.url)) return true;
|
|
91678
|
+
|
|
91679
|
+
// Knowledge-base files: resolved server-side from the source id.
|
|
91680
|
+
return Boolean(source.source_id);
|
|
91681
|
+
}
|
|
91682
|
+
|
|
91576
91683
|
function decodeName(name) {
|
|
91577
91684
|
if (!name) return "";
|
|
91578
91685
|
try {
|
|
@@ -91763,7 +91870,11 @@
|
|
|
91763
91870
|
const first = referenced[0];
|
|
91764
91871
|
const extraCount = referenced.length - 1;
|
|
91765
91872
|
const firstLabel = displayLabel(first.src, first.index);
|
|
91766
|
-
|
|
91873
|
+
// EV-4177: a card only gets the download affordance when a click will
|
|
91874
|
+
// actually produce a file. isFileLikeSource alone answers "is this a file",
|
|
91875
|
+
// not "can we fetch it" - which is how a download icon ended up on a source
|
|
91876
|
+
// that had no URL and no resolvable id behind it.
|
|
91877
|
+
const firstIsFile = isFileLikeSource(first.src) && canDownloadSource(first.src);
|
|
91767
91878
|
const busy = ctx.downloadingKey && ctx.downloadingKey === pillKey(first.src);
|
|
91768
91879
|
const activate = s => {
|
|
91769
91880
|
if (typeof ctx.onSourceActivate === "function") {
|
|
@@ -91922,7 +92033,8 @@
|
|
|
91922
92033
|
index
|
|
91923
92034
|
}) => {
|
|
91924
92035
|
const label = displayLabel(src, index);
|
|
91925
|
-
|
|
92036
|
+
// EV-4177: same gate as firstIsFile above.
|
|
92037
|
+
const rowIsFile = isFileLikeSource(src) && canDownloadSource(src);
|
|
91926
92038
|
const kBusy = ctx.downloadingKey && ctx.downloadingKey === pillKey(src);
|
|
91927
92039
|
return /*#__PURE__*/jsxRuntimeExports.jsxs("button", {
|
|
91928
92040
|
type: "button",
|