@eshal-bot/chat-widget 0.1.60 → 0.1.61
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 +8 -8
- package/dist/chat-widget.js +195 -43
- 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.js
CHANGED
|
@@ -7986,6 +7986,22 @@
|
|
|
7986
7986
|
* @typedef {function(string): void} MessageUpdater
|
|
7987
7987
|
*/
|
|
7988
7988
|
|
|
7989
|
+
/**
|
|
7990
|
+
* Thrown when the SSE stream's HTTP body closes without ever emitting a
|
|
7991
|
+
* terminal event (`complete` / `finish_reason`). The chat widget used to
|
|
7992
|
+
* silently return in this case, leaving the assistant bubble stuck in
|
|
7993
|
+
* isProcessing with whatever partial text had arrived. The caller catches
|
|
7994
|
+
* this specifically so it can either retry the request or surface a clear
|
|
7995
|
+
* "connection dropped" message to the user (UX-M08).
|
|
7996
|
+
*/
|
|
7997
|
+
class IncompleteStreamError extends Error {
|
|
7998
|
+
constructor() {
|
|
7999
|
+
let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'SSE stream ended before completion';
|
|
8000
|
+
super(message);
|
|
8001
|
+
this.name = 'IncompleteStreamError';
|
|
8002
|
+
}
|
|
8003
|
+
}
|
|
8004
|
+
|
|
7989
8005
|
// ============================================================================
|
|
7990
8006
|
// NETWORK
|
|
7991
8007
|
// ============================================================================
|
|
@@ -8273,6 +8289,11 @@
|
|
|
8273
8289
|
const hasReceivedFirstChunk = {
|
|
8274
8290
|
current: false
|
|
8275
8291
|
};
|
|
8292
|
+
// UX-M08: tracks whether the stream emitted any terminal event
|
|
8293
|
+
// (`complete` event or a finish_reason chunk). If the HTTP body
|
|
8294
|
+
// closes without one, the connection dropped mid-response and we
|
|
8295
|
+
// throw IncompleteStreamError so the caller can retry.
|
|
8296
|
+
let streamCompleted = false;
|
|
8276
8297
|
try {
|
|
8277
8298
|
while (true) {
|
|
8278
8299
|
const {
|
|
@@ -8345,6 +8366,7 @@
|
|
|
8345
8366
|
// Handle complete events
|
|
8346
8367
|
if (parsed.type === 'complete') {
|
|
8347
8368
|
var _ref2, _parsed$Sources, _parsed$choices2;
|
|
8369
|
+
streamCompleted = true;
|
|
8348
8370
|
// Normalize Sources from stream (API may send "Sources" or "sources")
|
|
8349
8371
|
const rawSources = (_ref2 = (_parsed$Sources = parsed.Sources) !== null && _parsed$Sources !== void 0 ? _parsed$Sources : parsed.sources) !== null && _ref2 !== void 0 ? _ref2 : [];
|
|
8350
8372
|
const sources = Array.isArray(rawSources) ? rawSources.map(s => {
|
|
@@ -8442,6 +8464,7 @@
|
|
|
8442
8464
|
|
|
8443
8465
|
// Handle finish_reason (stream complete)
|
|
8444
8466
|
if ((_parsed$choices4 = parsed.choices) !== null && _parsed$choices4 !== void 0 && (_parsed$choices4 = _parsed$choices4[0]) !== null && _parsed$choices4 !== void 0 && _parsed$choices4.finish_reason && parsed.choices[0].finish_reason !== null) {
|
|
8467
|
+
streamCompleted = true;
|
|
8445
8468
|
const metadata = {
|
|
8446
8469
|
prompts: parsed.prompts
|
|
8447
8470
|
};
|
|
@@ -8452,6 +8475,15 @@
|
|
|
8452
8475
|
} finally {
|
|
8453
8476
|
reader.releaseLock();
|
|
8454
8477
|
}
|
|
8478
|
+
|
|
8479
|
+
// UX-M08: HTTP body closed cleanly but no terminal event arrived —
|
|
8480
|
+
// treat as a dropped stream so the caller can retry. The `complete`
|
|
8481
|
+
// and finish_reason branches above are the only successful exits;
|
|
8482
|
+
// anything else means the network dropped or the server hung up
|
|
8483
|
+
// before sending the terminal event.
|
|
8484
|
+
if (!streamCompleted) {
|
|
8485
|
+
throw new IncompleteStreamError(accumulatedText.current ? 'Stream ended mid-response — partial content received' : 'Stream ended before any content was received');
|
|
8486
|
+
}
|
|
8455
8487
|
};
|
|
8456
8488
|
|
|
8457
8489
|
/**
|
|
@@ -9199,6 +9231,15 @@
|
|
|
9199
9231
|
const [onboardingAnswers, setOnboardingAnswers] = reactExports.useState({});
|
|
9200
9232
|
const pendingIntentProcessedRef = reactExports.useRef(false);
|
|
9201
9233
|
|
|
9234
|
+
// UX-M09: ref-based concurrency guard for sendMessage. The isLoading
|
|
9235
|
+
// state alone is not sufficient — setState is batched async, so two
|
|
9236
|
+
// rapid calls (e.g. the send button + an `onDirectSend` chip-click in
|
|
9237
|
+
// the same microtask) can both read isLoading=false and start two
|
|
9238
|
+
// overlapping SSE streams that interleave content into the same
|
|
9239
|
+
// assistant bubble. A ref flips synchronously and blocks the second
|
|
9240
|
+
// call before any state mutation runs.
|
|
9241
|
+
const isSendingRef = reactExports.useRef(false);
|
|
9242
|
+
|
|
9202
9243
|
// Bidi/Voice state
|
|
9203
9244
|
const [isConversationLoading, setIsConversationLoading] = reactExports.useState(false);
|
|
9204
9245
|
const [isVoiceSessionActive, setIsVoiceSessionActive] = reactExports.useState(false);
|
|
@@ -9572,6 +9613,17 @@
|
|
|
9572
9613
|
};
|
|
9573
9614
|
}
|
|
9574
9615
|
|
|
9616
|
+
// UX-M09: synchronous concurrency guard. See isSendingRef declaration
|
|
9617
|
+
// above for the full rationale — covers send-button, onDirectSend
|
|
9618
|
+
// chip clicks, and any other call site routed through sendMessage.
|
|
9619
|
+
if (isSendingRef.current) {
|
|
9620
|
+
return {
|
|
9621
|
+
success: false,
|
|
9622
|
+
error: 'A message is already being sent'
|
|
9623
|
+
};
|
|
9624
|
+
}
|
|
9625
|
+
isSendingRef.current = true;
|
|
9626
|
+
|
|
9575
9627
|
// PR-I + PR-P: cache the user's question so handleDecision can
|
|
9576
9628
|
// re-send it after a routing-clarification chip pick. Update
|
|
9577
9629
|
// BOTH the ref (synchronously, for the setTimeout closure) and
|
|
@@ -9581,6 +9633,7 @@
|
|
|
9581
9633
|
setPendingRoutingQuestion(trimmed);
|
|
9582
9634
|
}
|
|
9583
9635
|
if (!apiBaseUrl) {
|
|
9636
|
+
isSendingRef.current = false;
|
|
9584
9637
|
setMessages(prev => {
|
|
9585
9638
|
const errorMessage = createMessage({
|
|
9586
9639
|
id: getNextMessageId(),
|
|
@@ -9699,11 +9752,20 @@
|
|
|
9699
9752
|
})
|
|
9700
9753
|
});
|
|
9701
9754
|
|
|
9702
|
-
//
|
|
9703
|
-
|
|
9755
|
+
// UX-M08: retry the stream up to MAX_STREAM_RETRIES times if the
|
|
9756
|
+
// server hangs up mid-response (IncompleteStreamError). Each retry
|
|
9757
|
+
// re-issues the POST and re-runs processStream. The skeleton
|
|
9758
|
+
// bubble is cleared between attempts so a partial assistant
|
|
9759
|
+
// message from the dropped attempt doesn't accumulate alongside
|
|
9760
|
+
// the retried one. Retry only applies to chat-message types —
|
|
9761
|
+
// onboarding-field sends don't need it (they consume the body
|
|
9762
|
+
// raw and don't surface partial state to the user).
|
|
9763
|
+
const STREAM_RETRY_DELAYS_MS = [500, 1500]; // → 2 retries total
|
|
9704
9764
|
|
|
9705
|
-
// For onboarding fields, just consume the stream without processing
|
|
9706
9765
|
if (apiType === 'userName' || apiType === 'email' || apiType === 'phone') {
|
|
9766
|
+
// For onboarding fields, just consume the stream without processing.
|
|
9767
|
+
// Single attempt — no retry, no user-visible partial state to clean up.
|
|
9768
|
+
const stream = await sendStreamingRequest(chatRequest, apiBaseUrl, organizationId);
|
|
9707
9769
|
const reader = stream.getReader();
|
|
9708
9770
|
const decoder = new TextDecoder();
|
|
9709
9771
|
while (true) {
|
|
@@ -9718,15 +9780,39 @@
|
|
|
9718
9780
|
success: true
|
|
9719
9781
|
};
|
|
9720
9782
|
}
|
|
9721
|
-
|
|
9722
|
-
// For regular messages, process the stream
|
|
9723
9783
|
if (apiType === 'inquiry' || apiType === 'complaint' || apiType === 'technical') {
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9784
|
+
let lastErr;
|
|
9785
|
+
for (let attempt = 0; attempt <= STREAM_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
9786
|
+
try {
|
|
9787
|
+
const stream = await sendStreamingRequest(chatRequest, apiBaseUrl, organizationId);
|
|
9788
|
+
await processStream(stream, setMessages);
|
|
9789
|
+
updateActivity(organizationId);
|
|
9790
|
+
return {
|
|
9791
|
+
success: true
|
|
9792
|
+
};
|
|
9793
|
+
} catch (err) {
|
|
9794
|
+
lastErr = err;
|
|
9795
|
+
if (err instanceof IncompleteStreamError && attempt < STREAM_RETRY_DELAYS_MS.length) {
|
|
9796
|
+
// Drop the half-rendered assistant bubble before retrying so
|
|
9797
|
+
// the next attempt doesn't paint into a stale skeleton.
|
|
9798
|
+
setMessages(prev => (prev || []).filter(msg => !msg.isProcessing));
|
|
9799
|
+
const delay = STREAM_RETRY_DELAYS_MS[attempt];
|
|
9800
|
+
console.warn("[chat] stream dropped (attempt ".concat(attempt + 1, "); retrying in ").concat(delay, "ms \u2014 ").concat(err.message));
|
|
9801
|
+
await new Promise(r => setTimeout(r, delay));
|
|
9802
|
+
continue;
|
|
9803
|
+
}
|
|
9804
|
+
throw err;
|
|
9805
|
+
}
|
|
9806
|
+
}
|
|
9807
|
+
// Unreachable under normal flow — the loop either returns
|
|
9808
|
+
// success, throws, or continues. Throw the last seen error
|
|
9809
|
+
// defensively so we never fall through silently.
|
|
9810
|
+
throw lastErr;
|
|
9729
9811
|
}
|
|
9812
|
+
|
|
9813
|
+
// Non-chat, non-onboarding apiType (shouldn't currently happen):
|
|
9814
|
+
// fire and forget, single attempt.
|
|
9815
|
+
await sendStreamingRequest(chatRequest, apiBaseUrl, organizationId);
|
|
9730
9816
|
updateActivity(organizationId);
|
|
9731
9817
|
return {
|
|
9732
9818
|
success: true
|
|
@@ -9751,9 +9837,16 @@
|
|
|
9751
9837
|
error: error instanceof Error ? error.message : 'Failed to send message'
|
|
9752
9838
|
};
|
|
9753
9839
|
} finally {
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9840
|
+
// Reset isLoading unconditionally — onboarding field sends
|
|
9841
|
+
// (userName/email/phone) used to skip this branch, so any ambient
|
|
9842
|
+
// loading state set elsewhere (voice flow, parent loop) was left
|
|
9843
|
+
// stuck true after the onboarding step resolved and blocked further
|
|
9844
|
+
// input until the next regular message ran. setIsLoading(false) is
|
|
9845
|
+
// a no-op when it's already false, so widening the condition can't
|
|
9846
|
+
// regress the inquiry/complaint/technical path.
|
|
9847
|
+
setIsLoading(false);
|
|
9848
|
+
// UX-M09: release the concurrency guard so the next send can proceed.
|
|
9849
|
+
isSendingRef.current = false;
|
|
9757
9850
|
}
|
|
9758
9851
|
}, [apiBaseUrl, apiKey, getNextMessageId, organizationId, messages, bidiSessionId, routingChoice, widgetFilter, provideCitations]);
|
|
9759
9852
|
const handleOnboardingFormSubmit = reactExports.useCallback(async answers => {
|
|
@@ -91638,16 +91731,16 @@
|
|
|
91638
91731
|
formatAxisMap: formatAxisMap
|
|
91639
91732
|
});
|
|
91640
91733
|
|
|
91641
|
-
/**
|
|
91642
|
-
* Widget A2UI catalog — Recharts-based BarChart and KPICard renderers.
|
|
91643
|
-
*
|
|
91644
|
-
* Mirrors the agent-sdk's catalog.tsx renderers (ported to plain JS +
|
|
91645
|
-
* dark-mode aware). These are called directly from A2uiBlock's dispatch
|
|
91646
|
-
* for bar/kpi types, replacing the hand-rolled SVG implementations.
|
|
91647
|
-
*
|
|
91648
|
-
* The renderers accept the same `{ props }` interface as CopilotKit's
|
|
91649
|
-
* catalog convention so they remain drop-in compatible if we ever wire up
|
|
91650
|
-
* the full A2UIProvider pipeline here.
|
|
91734
|
+
/**
|
|
91735
|
+
* Widget A2UI catalog — Recharts-based BarChart and KPICard renderers.
|
|
91736
|
+
*
|
|
91737
|
+
* Mirrors the agent-sdk's catalog.tsx renderers (ported to plain JS +
|
|
91738
|
+
* dark-mode aware). These are called directly from A2uiBlock's dispatch
|
|
91739
|
+
* for bar/kpi types, replacing the hand-rolled SVG implementations.
|
|
91740
|
+
*
|
|
91741
|
+
* The renderers accept the same `{ props }` interface as CopilotKit's
|
|
91742
|
+
* catalog convention so they remain drop-in compatible if we ever wire up
|
|
91743
|
+
* the full A2UIProvider pipeline here.
|
|
91651
91744
|
*/
|
|
91652
91745
|
|
|
91653
91746
|
function formatCompactNumber(n) {
|
|
@@ -91656,11 +91749,11 @@
|
|
|
91656
91749
|
return String(n);
|
|
91657
91750
|
}
|
|
91658
91751
|
|
|
91659
|
-
/**
|
|
91660
|
-
* Recharts bar chart — mirrors agent-sdk's eshalRenderers.BarChart.
|
|
91661
|
-
*
|
|
91662
|
-
* Expects `props.data` to be an array of `{ label, value }` objects
|
|
91663
|
-
* (already normalized by A2uiBlock's numericData helper).
|
|
91752
|
+
/**
|
|
91753
|
+
* Recharts bar chart — mirrors agent-sdk's eshalRenderers.BarChart.
|
|
91754
|
+
*
|
|
91755
|
+
* Expects `props.data` to be an array of `{ label, value }` objects
|
|
91756
|
+
* (already normalized by A2uiBlock's numericData helper).
|
|
91664
91757
|
*/
|
|
91665
91758
|
function BarChartRenderer(_ref) {
|
|
91666
91759
|
let {
|
|
@@ -91730,7 +91823,7 @@
|
|
|
91730
91823
|
margin: {
|
|
91731
91824
|
top: 8,
|
|
91732
91825
|
right: 12,
|
|
91733
|
-
left:
|
|
91826
|
+
left: 12,
|
|
91734
91827
|
bottom: 20
|
|
91735
91828
|
},
|
|
91736
91829
|
children: [/*#__PURE__*/jsxRuntimeExports.jsx(CartesianGrid, {
|
|
@@ -91750,6 +91843,7 @@
|
|
|
91750
91843
|
fill: labelColor
|
|
91751
91844
|
} : undefined
|
|
91752
91845
|
}), /*#__PURE__*/jsxRuntimeExports.jsx(YAxis, {
|
|
91846
|
+
width: 72,
|
|
91753
91847
|
tick: {
|
|
91754
91848
|
fontSize: 11,
|
|
91755
91849
|
fill: labelColor
|
|
@@ -91782,9 +91876,9 @@
|
|
|
91782
91876
|
});
|
|
91783
91877
|
}
|
|
91784
91878
|
|
|
91785
|
-
/**
|
|
91786
|
-
* KPI card — mirrors agent-sdk's eshalRenderers.KPICard.
|
|
91787
|
-
* Dark-mode aware.
|
|
91879
|
+
/**
|
|
91880
|
+
* KPI card — mirrors agent-sdk's eshalRenderers.KPICard.
|
|
91881
|
+
* Dark-mode aware.
|
|
91788
91882
|
*/
|
|
91789
91883
|
function KPICardRenderer(_ref2) {
|
|
91790
91884
|
let {
|
|
@@ -91844,10 +91938,10 @@
|
|
|
91844
91938
|
}
|
|
91845
91939
|
const PIE_COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316', '#6366f1', '#84cc16', '#06b6d4', '#a855f7'];
|
|
91846
91940
|
|
|
91847
|
-
/**
|
|
91848
|
-
* Recharts pie / donut chart — replaces hand-rolled PieSvg.
|
|
91849
|
-
* `props.data` must be an array of `{ label, value }` objects.
|
|
91850
|
-
* Set `donut={true}` for a donut chart (innerRadius > 0).
|
|
91941
|
+
/**
|
|
91942
|
+
* Recharts pie / donut chart — replaces hand-rolled PieSvg.
|
|
91943
|
+
* `props.data` must be an array of `{ label, value }` objects.
|
|
91944
|
+
* Set `donut={true}` for a donut chart (innerRadius > 0).
|
|
91851
91945
|
*/
|
|
91852
91946
|
function PieChartRenderer(_ref3) {
|
|
91853
91947
|
let {
|
|
@@ -91928,10 +92022,10 @@
|
|
|
91928
92022
|
});
|
|
91929
92023
|
}
|
|
91930
92024
|
|
|
91931
|
-
/**
|
|
91932
|
-
* Recharts line / area chart — replaces hand-rolled LineSvg.
|
|
91933
|
-
* `props.data` must be an array of `{ label, value }` objects.
|
|
91934
|
-
* Set `area={true}` to fill under the line.
|
|
92025
|
+
/**
|
|
92026
|
+
* Recharts line / area chart — replaces hand-rolled LineSvg.
|
|
92027
|
+
* `props.data` must be an array of `{ label, value }` objects.
|
|
92028
|
+
* Set `area={true}` to fill under the line.
|
|
91935
92029
|
*/
|
|
91936
92030
|
function LineChartRenderer(_ref4) {
|
|
91937
92031
|
let {
|
|
@@ -92003,7 +92097,7 @@
|
|
|
92003
92097
|
margin: {
|
|
92004
92098
|
top: 8,
|
|
92005
92099
|
right: 12,
|
|
92006
|
-
left:
|
|
92100
|
+
left: 12,
|
|
92007
92101
|
bottom: 20
|
|
92008
92102
|
},
|
|
92009
92103
|
children: [/*#__PURE__*/jsxRuntimeExports.jsx(CartesianGrid, {
|
|
@@ -92023,6 +92117,7 @@
|
|
|
92023
92117
|
fill: labelColor
|
|
92024
92118
|
} : undefined
|
|
92025
92119
|
}), /*#__PURE__*/jsxRuntimeExports.jsx(YAxis, {
|
|
92120
|
+
width: 72,
|
|
92026
92121
|
tick: {
|
|
92027
92122
|
fontSize: 11,
|
|
92028
92123
|
fill: labelColor
|
|
@@ -92573,14 +92668,52 @@
|
|
|
92573
92668
|
return typeof key === 'string' && CURRENCY_KEY_RE.test(key);
|
|
92574
92669
|
}
|
|
92575
92670
|
|
|
92671
|
+
// ISO date / datetime prefix — matches the typical SQL output the
|
|
92672
|
+
// agent's tools produce when GROUP BY-ing by month
|
|
92673
|
+
// (`DATE_TRUNC('month', col)` → `YYYY-MM-DD`). Used by formatAxisTick
|
|
92674
|
+
// below so a chart of monthly expenses shows "Jan '26" / "Feb '26"
|
|
92675
|
+
// horizontally instead of "2026 01 01" slanted (EV-1187 follow-up).
|
|
92676
|
+
const ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})(?:T|$)/;
|
|
92677
|
+
function formatIsoDateAsMonthTick(value) {
|
|
92678
|
+
if (typeof value !== 'string') return null;
|
|
92679
|
+
const m = value.match(ISO_DATE_RE);
|
|
92680
|
+
if (!m) return null;
|
|
92681
|
+
const year = parseInt(m[1], 10);
|
|
92682
|
+
const month = parseInt(m[2], 10);
|
|
92683
|
+
if (!Number.isInteger(month) || month < 1 || month > 12) return null;
|
|
92684
|
+
return "".concat(MONTH_NAMES[month - 1], " '").concat(String(year).slice(-2));
|
|
92685
|
+
}
|
|
92686
|
+
|
|
92576
92687
|
// Axis-tick formatter: month-number columns (column matches /month/i,
|
|
92577
|
-
// value 1..12) render as 3-letter month names;
|
|
92688
|
+
// value 1..12) render as 3-letter month names; ISO date strings
|
|
92689
|
+
// (`2026-01-01`) render as "Jan '26"; everything else gets
|
|
92578
92690
|
// prettifyLabel applied so "office_supplies" reads "Office Supplies".
|
|
92579
92691
|
function formatAxisTick(value, dataKey) {
|
|
92580
92692
|
if (typeof dataKey === 'string' && MONTH_KEY_RE.test(dataKey)) {
|
|
92581
92693
|
const n = typeof value === 'number' ? value : Number(value);
|
|
92582
92694
|
if (Number.isInteger(n) && n >= 1 && n <= 12) return MONTH_NAMES[n - 1];
|
|
92583
92695
|
}
|
|
92696
|
+
const dateMonth = formatIsoDateAsMonthTick(value);
|
|
92697
|
+
if (dateMonth) return dateMonth;
|
|
92698
|
+
return prettifyLabel(value);
|
|
92699
|
+
}
|
|
92700
|
+
|
|
92701
|
+
// Conservative snake_case detector for table cell category labels
|
|
92702
|
+
// (2026-06-19, EV-1187 follow-up). Mirrors the admin UI's
|
|
92703
|
+
// `prettifyCategoryCell` in `packages/frontend/.../chartShared.tsx` and
|
|
92704
|
+
// the SDK's copy. Self-contained because the widget can't reach across
|
|
92705
|
+
// package boundaries.
|
|
92706
|
+
//
|
|
92707
|
+
// Goal: when a category column shows `office_supplies` next to a chart
|
|
92708
|
+
// whose X-axis (via prettifyLabel) already renders "Office Supplies",
|
|
92709
|
+
// the table should agree. Conservative — only lowercase ASCII tokens
|
|
92710
|
+
// joined by underscores (≤ 40 chars) qualify, so IDs/paths/emails/
|
|
92711
|
+
// slugs/camelCase pass through verbatim.
|
|
92712
|
+
const SNAKE_CASE_CATEGORY_RE = /^[a-z]+(_[a-z]+)+$/;
|
|
92713
|
+
function prettifyCategoryCell(value) {
|
|
92714
|
+
if (typeof value !== 'string') return String(value !== null && value !== void 0 ? value : '');
|
|
92715
|
+
if (value.length > 40) return value;
|
|
92716
|
+
if (!SNAKE_CASE_CATEGORY_RE.test(value)) return value;
|
|
92584
92717
|
return prettifyLabel(value);
|
|
92585
92718
|
}
|
|
92586
92719
|
function formatCell(v, columnKey) {
|
|
@@ -92592,7 +92725,12 @@
|
|
|
92592
92725
|
return Number.isInteger(v) ? String(v) : v.toFixed(2);
|
|
92593
92726
|
}
|
|
92594
92727
|
if (typeof v === 'object') return JSON.stringify(v);
|
|
92595
|
-
|
|
92728
|
+
// String cells: prettify snake_case category labels (e.g.
|
|
92729
|
+
// `office_supplies` → "Office Supplies") so the table reads
|
|
92730
|
+
// consistently with the chart's X-axis ticks on the same data.
|
|
92731
|
+
// Conservative: anything not a clean lowercase_underscore token is
|
|
92732
|
+
// returned verbatim, so paths/IDs/emails/slugs are preserved.
|
|
92733
|
+
return prettifyCategoryCell(v);
|
|
92596
92734
|
}
|
|
92597
92735
|
|
|
92598
92736
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -93626,8 +93764,22 @@
|
|
|
93626
93764
|
// When streaming, hide trailing table lines that haven't formed a complete
|
|
93627
93765
|
// table yet (no separator row). Without this, partial `| col |` lines flash
|
|
93628
93766
|
// as raw pipe text before ReactMarkdown can recognise the block as a table.
|
|
93767
|
+
// Also hides incomplete A2UI fences (```eshal-ui / ```eshal-chart / ```chart)
|
|
93768
|
+
// that haven't received their closing ``` yet — prevents raw JSON flash.
|
|
93629
93769
|
function sanitizeStreamingContent(text, isStreaming) {
|
|
93630
93770
|
if (!isStreaming) return text;
|
|
93771
|
+
|
|
93772
|
+
// A2UI fence guard: if an opening fence exists without a matching closing
|
|
93773
|
+
// ```, suppress everything from that opening onwards so the partial JSON/
|
|
93774
|
+
// code never flashes as raw text in the bubble.
|
|
93775
|
+
const openMatch = /^```(?:eshal-ui|eshal-chart|chart)\b/m.exec(text);
|
|
93776
|
+
if (openMatch) {
|
|
93777
|
+
const rest = text.slice(openMatch.index + openMatch[0].length);
|
|
93778
|
+
const hasClose = rest.split('\n').some(l => l.trim() === '```');
|
|
93779
|
+
if (!hasClose) {
|
|
93780
|
+
return text.slice(0, openMatch.index).trimEnd();
|
|
93781
|
+
}
|
|
93782
|
+
}
|
|
93631
93783
|
const lines = text.split('\n');
|
|
93632
93784
|
// Find last non-empty line index
|
|
93633
93785
|
let last = lines.length - 1;
|