@eshal-bot/chat-widget 0.1.60 → 0.1.62
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.esm.js.map +1 -0
- package/dist/chat-widget.js +257 -49
- package/dist/chat-widget.js.map +1 -0
- package/dist/chat-widget.min.js +6 -6
- package/dist/chat-widget.umd.js +8 -8
- package/dist/chat-widget.umd.js.map +1 -0
- package/package.json +93 -93
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,36 @@
|
|
|
91656
91749
|
return String(n);
|
|
91657
91750
|
}
|
|
91658
91751
|
|
|
91659
|
-
|
|
91660
|
-
|
|
91661
|
-
|
|
91662
|
-
|
|
91663
|
-
|
|
91752
|
+
// ── Currency-aware value formatting ──────────────────────────────────────
|
|
91753
|
+
// A2UI chart data is normalized to `{ label, value }` by A2uiBlock's
|
|
91754
|
+
// numericData(), which drops the original Y-column key. A2uiBlock now threads
|
|
91755
|
+
// that key through as `props.yKey` so these renderers can decide whether the
|
|
91756
|
+
// axis / tooltip values are money. Heuristic mirrors the frontend's
|
|
91757
|
+
// chartShared.tsx `isCurrencyKey` — keep the keyword list in lockstep.
|
|
91758
|
+
// `(^|[_\W])` (not `\b`) so "average_salary" / "total_expenses" still match —
|
|
91759
|
+
// JS `\b` doesn't fire next to `_`.
|
|
91760
|
+
const CURRENCY_KEY_RE$1 = /(^|[_\W])(price|salary|cost|amount|revenue|expense|total|value|budget|spend|income|sale|balance)/i;
|
|
91761
|
+
function isCurrencyKey$1(key) {
|
|
91762
|
+
return typeof key === 'string' && CURRENCY_KEY_RE$1.test(key);
|
|
91763
|
+
}
|
|
91764
|
+
|
|
91765
|
+
// Compact currency for axis ticks → "$120.0k". Full currency for tooltips →
|
|
91766
|
+
// "$87,600" (comma-grouped, up to 2 decimals, no forced trailing zeros so a
|
|
91767
|
+
// whole-dollar salary reads "$81,200" not "$81,200.00").
|
|
91768
|
+
function formatCurrencyCompact(v) {
|
|
91769
|
+
return "$".concat(formatCompactNumber(v));
|
|
91770
|
+
}
|
|
91771
|
+
function formatCurrencyFull(v) {
|
|
91772
|
+
return "$".concat(Number(v).toLocaleString('en-US', {
|
|
91773
|
+
maximumFractionDigits: 2
|
|
91774
|
+
}));
|
|
91775
|
+
}
|
|
91776
|
+
|
|
91777
|
+
/**
|
|
91778
|
+
* Recharts bar chart — mirrors agent-sdk's eshalRenderers.BarChart.
|
|
91779
|
+
*
|
|
91780
|
+
* Expects `props.data` to be an array of `{ label, value }` objects
|
|
91781
|
+
* (already normalized by A2uiBlock's numericData helper).
|
|
91664
91782
|
*/
|
|
91665
91783
|
function BarChartRenderer(_ref) {
|
|
91666
91784
|
let {
|
|
@@ -91668,6 +91786,11 @@
|
|
|
91668
91786
|
isDark = false
|
|
91669
91787
|
} = _ref;
|
|
91670
91788
|
const data = Array.isArray(props.data) ? props.data : [];
|
|
91789
|
+
// Whether the Y values are money — drives the `$` on axis ticks + tooltip.
|
|
91790
|
+
const currency = isCurrencyKey$1(props.yKey);
|
|
91791
|
+
// Human series name for the tooltip ("Average Salary"), so it no longer
|
|
91792
|
+
// reads the generic dataKey "value".
|
|
91793
|
+
const seriesName = props.valueLabel || undefined;
|
|
91671
91794
|
const borderColor = isDark ? '#374151' : '#e2e8f0';
|
|
91672
91795
|
const bgColor = isDark ? '#1f2937' : '#ffffff';
|
|
91673
91796
|
const labelColor = isDark ? '#9ca3af' : '#64748b';
|
|
@@ -91730,7 +91853,7 @@
|
|
|
91730
91853
|
margin: {
|
|
91731
91854
|
top: 8,
|
|
91732
91855
|
right: 12,
|
|
91733
|
-
left:
|
|
91856
|
+
left: 12,
|
|
91734
91857
|
bottom: 20
|
|
91735
91858
|
},
|
|
91736
91859
|
children: [/*#__PURE__*/jsxRuntimeExports.jsx(CartesianGrid, {
|
|
@@ -91750,6 +91873,7 @@
|
|
|
91750
91873
|
fill: labelColor
|
|
91751
91874
|
} : undefined
|
|
91752
91875
|
}), /*#__PURE__*/jsxRuntimeExports.jsx(YAxis, {
|
|
91876
|
+
width: 72,
|
|
91753
91877
|
tick: {
|
|
91754
91878
|
fontSize: 11,
|
|
91755
91879
|
fill: labelColor
|
|
@@ -91761,9 +91885,9 @@
|
|
|
91761
91885
|
fontSize: 11,
|
|
91762
91886
|
fill: labelColor
|
|
91763
91887
|
} : undefined,
|
|
91764
|
-
tickFormatter: v => props.unit ? "".concat(formatCompactNumber(v), " ").concat(props.unit) : formatCompactNumber(v)
|
|
91888
|
+
tickFormatter: v => currency ? formatCurrencyCompact(v) : props.unit ? "".concat(formatCompactNumber(v), " ").concat(props.unit) : formatCompactNumber(v)
|
|
91765
91889
|
}), /*#__PURE__*/jsxRuntimeExports.jsx(Tooltip, {
|
|
91766
|
-
formatter: v => props.unit ? "".concat(Number(v).toLocaleString(), " ").concat(props.unit) : Number(v).toLocaleString(),
|
|
91890
|
+
formatter: v => currency ? formatCurrencyFull(v) : props.unit ? "".concat(Number(v).toLocaleString(), " ").concat(props.unit) : Number(v).toLocaleString(),
|
|
91767
91891
|
contentStyle: {
|
|
91768
91892
|
background: tooltipBg,
|
|
91769
91893
|
border: "1px solid ".concat(borderColor),
|
|
@@ -91773,6 +91897,7 @@
|
|
|
91773
91897
|
}
|
|
91774
91898
|
}), /*#__PURE__*/jsxRuntimeExports.jsx(Bar, {
|
|
91775
91899
|
dataKey: "value",
|
|
91900
|
+
name: seriesName,
|
|
91776
91901
|
fill: "#3b82f6",
|
|
91777
91902
|
radius: [2, 2, 0, 0]
|
|
91778
91903
|
})]
|
|
@@ -91782,9 +91907,9 @@
|
|
|
91782
91907
|
});
|
|
91783
91908
|
}
|
|
91784
91909
|
|
|
91785
|
-
/**
|
|
91786
|
-
* KPI card — mirrors agent-sdk's eshalRenderers.KPICard.
|
|
91787
|
-
* Dark-mode aware.
|
|
91910
|
+
/**
|
|
91911
|
+
* KPI card — mirrors agent-sdk's eshalRenderers.KPICard.
|
|
91912
|
+
* Dark-mode aware.
|
|
91788
91913
|
*/
|
|
91789
91914
|
function KPICardRenderer(_ref2) {
|
|
91790
91915
|
let {
|
|
@@ -91844,10 +91969,10 @@
|
|
|
91844
91969
|
}
|
|
91845
91970
|
const PIE_COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316', '#6366f1', '#84cc16', '#06b6d4', '#a855f7'];
|
|
91846
91971
|
|
|
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).
|
|
91972
|
+
/**
|
|
91973
|
+
* Recharts pie / donut chart — replaces hand-rolled PieSvg.
|
|
91974
|
+
* `props.data` must be an array of `{ label, value }` objects.
|
|
91975
|
+
* Set `donut={true}` for a donut chart (innerRadius > 0).
|
|
91851
91976
|
*/
|
|
91852
91977
|
function PieChartRenderer(_ref3) {
|
|
91853
91978
|
let {
|
|
@@ -91856,6 +91981,7 @@
|
|
|
91856
91981
|
donut = false
|
|
91857
91982
|
} = _ref3;
|
|
91858
91983
|
const data = Array.isArray(props.data) ? props.data : [];
|
|
91984
|
+
const currency = isCurrencyKey$1(props.yKey);
|
|
91859
91985
|
const borderColor = isDark ? '#374151' : '#e2e8f0';
|
|
91860
91986
|
const bgColor = isDark ? '#1f2937' : '#ffffff';
|
|
91861
91987
|
const titleColor = isDark ? '#f3f4f6' : '#0f172a';
|
|
@@ -91906,7 +92032,7 @@
|
|
|
91906
92032
|
fill: PIE_COLORS[i % PIE_COLORS.length]
|
|
91907
92033
|
}, i))
|
|
91908
92034
|
}), /*#__PURE__*/jsxRuntimeExports.jsx(Tooltip, {
|
|
91909
|
-
formatter: v => Number(v).toLocaleString(),
|
|
92035
|
+
formatter: v => currency ? formatCurrencyFull(v) : Number(v).toLocaleString(),
|
|
91910
92036
|
contentStyle: {
|
|
91911
92037
|
background: tooltipBg,
|
|
91912
92038
|
border: "1px solid ".concat(borderColor),
|
|
@@ -91928,10 +92054,10 @@
|
|
|
91928
92054
|
});
|
|
91929
92055
|
}
|
|
91930
92056
|
|
|
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.
|
|
92057
|
+
/**
|
|
92058
|
+
* Recharts line / area chart — replaces hand-rolled LineSvg.
|
|
92059
|
+
* `props.data` must be an array of `{ label, value }` objects.
|
|
92060
|
+
* Set `area={true}` to fill under the line.
|
|
91935
92061
|
*/
|
|
91936
92062
|
function LineChartRenderer(_ref4) {
|
|
91937
92063
|
let {
|
|
@@ -91940,6 +92066,8 @@
|
|
|
91940
92066
|
area = false
|
|
91941
92067
|
} = _ref4;
|
|
91942
92068
|
const data = Array.isArray(props.data) ? props.data : [];
|
|
92069
|
+
const currency = isCurrencyKey$1(props.yKey);
|
|
92070
|
+
const seriesName = props.valueLabel || undefined;
|
|
91943
92071
|
const borderColor = isDark ? '#374151' : '#e2e8f0';
|
|
91944
92072
|
const bgColor = isDark ? '#1f2937' : '#ffffff';
|
|
91945
92073
|
const labelColor = isDark ? '#9ca3af' : '#64748b';
|
|
@@ -92003,7 +92131,7 @@
|
|
|
92003
92131
|
margin: {
|
|
92004
92132
|
top: 8,
|
|
92005
92133
|
right: 12,
|
|
92006
|
-
left:
|
|
92134
|
+
left: 12,
|
|
92007
92135
|
bottom: 20
|
|
92008
92136
|
},
|
|
92009
92137
|
children: [/*#__PURE__*/jsxRuntimeExports.jsx(CartesianGrid, {
|
|
@@ -92023,13 +92151,14 @@
|
|
|
92023
92151
|
fill: labelColor
|
|
92024
92152
|
} : undefined
|
|
92025
92153
|
}), /*#__PURE__*/jsxRuntimeExports.jsx(YAxis, {
|
|
92154
|
+
width: 72,
|
|
92026
92155
|
tick: {
|
|
92027
92156
|
fontSize: 11,
|
|
92028
92157
|
fill: labelColor
|
|
92029
92158
|
},
|
|
92030
|
-
tickFormatter: v => props.unit ? "".concat(formatCompactNumber(v), " ").concat(props.unit) : formatCompactNumber(v)
|
|
92159
|
+
tickFormatter: v => currency ? formatCurrencyCompact(v) : props.unit ? "".concat(formatCompactNumber(v), " ").concat(props.unit) : formatCompactNumber(v)
|
|
92031
92160
|
}), /*#__PURE__*/jsxRuntimeExports.jsx(Tooltip, {
|
|
92032
|
-
formatter: v => props.unit ? "".concat(Number(v).toLocaleString(), " ").concat(props.unit) : Number(v).toLocaleString(),
|
|
92161
|
+
formatter: v => currency ? formatCurrencyFull(v) : props.unit ? "".concat(Number(v).toLocaleString(), " ").concat(props.unit) : Number(v).toLocaleString(),
|
|
92033
92162
|
contentStyle: {
|
|
92034
92163
|
background: tooltipBg,
|
|
92035
92164
|
border: "1px solid ".concat(borderColor),
|
|
@@ -92040,6 +92169,7 @@
|
|
|
92040
92169
|
}), area ? /*#__PURE__*/jsxRuntimeExports.jsx(Area, {
|
|
92041
92170
|
type: "monotone",
|
|
92042
92171
|
dataKey: "value",
|
|
92172
|
+
name: seriesName,
|
|
92043
92173
|
stroke: "#3b82f6",
|
|
92044
92174
|
fill: "#3b82f6",
|
|
92045
92175
|
fillOpacity: 0.15,
|
|
@@ -92051,6 +92181,7 @@
|
|
|
92051
92181
|
}) : /*#__PURE__*/jsxRuntimeExports.jsx(Line, {
|
|
92052
92182
|
type: "monotone",
|
|
92053
92183
|
dataKey: "value",
|
|
92184
|
+
name: seriesName,
|
|
92054
92185
|
stroke: "#3b82f6",
|
|
92055
92186
|
strokeWidth: 2,
|
|
92056
92187
|
dot: {
|
|
@@ -92248,7 +92379,14 @@
|
|
|
92248
92379
|
data: rows,
|
|
92249
92380
|
unit: spec.unit,
|
|
92250
92381
|
xAxisLabel: spec.xAxisLabel,
|
|
92251
|
-
yAxisLabel: spec.yAxisLabel
|
|
92382
|
+
yAxisLabel: spec.yAxisLabel,
|
|
92383
|
+
// numericData() collapses the rows to { label, value }, dropping the
|
|
92384
|
+
// original Y-column key. Pass it through so the renderer can detect a
|
|
92385
|
+
// currency column (→ "$" on the Y-axis + tooltip) and label the tooltip
|
|
92386
|
+
// series with the human column name ("Average Salary") instead of the
|
|
92387
|
+
// generic dataKey "value".
|
|
92388
|
+
yKey: y,
|
|
92389
|
+
valueLabel: prettifyLabel(y)
|
|
92252
92390
|
};
|
|
92253
92391
|
if (t === 'pie') return /*#__PURE__*/jsxRuntimeExports.jsx(PieChartRenderer, {
|
|
92254
92392
|
props: chartProps,
|
|
@@ -92573,16 +92711,62 @@
|
|
|
92573
92711
|
return typeof key === 'string' && CURRENCY_KEY_RE.test(key);
|
|
92574
92712
|
}
|
|
92575
92713
|
|
|
92714
|
+
// ISO date / datetime prefix — matches the typical SQL output the
|
|
92715
|
+
// agent's tools produce when GROUP BY-ing by month
|
|
92716
|
+
// (`DATE_TRUNC('month', col)` → `YYYY-MM-DD`). Used by formatAxisTick
|
|
92717
|
+
// below so a chart of monthly expenses shows "Jan '26" / "Feb '26"
|
|
92718
|
+
// horizontally instead of "2026 01 01" slanted (EV-1187 follow-up).
|
|
92719
|
+
const ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})(?:T|$)/;
|
|
92720
|
+
function formatIsoDateAsMonthTick(value) {
|
|
92721
|
+
if (typeof value !== 'string') return null;
|
|
92722
|
+
const m = value.match(ISO_DATE_RE);
|
|
92723
|
+
if (!m) return null;
|
|
92724
|
+
const year = parseInt(m[1], 10);
|
|
92725
|
+
const month = parseInt(m[2], 10);
|
|
92726
|
+
if (!Number.isInteger(month) || month < 1 || month > 12) return null;
|
|
92727
|
+
return "".concat(MONTH_NAMES[month - 1], " '").concat(String(year).slice(-2));
|
|
92728
|
+
}
|
|
92729
|
+
|
|
92576
92730
|
// Axis-tick formatter: month-number columns (column matches /month/i,
|
|
92577
|
-
// value 1..12) render as 3-letter month names;
|
|
92731
|
+
// value 1..12) render as 3-letter month names; ISO date strings
|
|
92732
|
+
// (`2026-01-01`) render as "Jan '26"; everything else gets
|
|
92578
92733
|
// prettifyLabel applied so "office_supplies" reads "Office Supplies".
|
|
92579
92734
|
function formatAxisTick(value, dataKey) {
|
|
92580
92735
|
if (typeof dataKey === 'string' && MONTH_KEY_RE.test(dataKey)) {
|
|
92581
92736
|
const n = typeof value === 'number' ? value : Number(value);
|
|
92582
92737
|
if (Number.isInteger(n) && n >= 1 && n <= 12) return MONTH_NAMES[n - 1];
|
|
92583
92738
|
}
|
|
92739
|
+
const dateMonth = formatIsoDateAsMonthTick(value);
|
|
92740
|
+
if (dateMonth) return dateMonth;
|
|
92584
92741
|
return prettifyLabel(value);
|
|
92585
92742
|
}
|
|
92743
|
+
|
|
92744
|
+
// Conservative snake_case detector for table cell category labels
|
|
92745
|
+
// (2026-06-19, EV-1187 follow-up). Mirrors the admin UI's
|
|
92746
|
+
// `prettifyCategoryCell` in `packages/frontend/.../chartShared.tsx` and
|
|
92747
|
+
// the SDK's copy. Self-contained because the widget can't reach across
|
|
92748
|
+
// package boundaries.
|
|
92749
|
+
//
|
|
92750
|
+
// Goal: when a category column shows `office_supplies` next to a chart
|
|
92751
|
+
// whose X-axis (via prettifyLabel) already renders "Office Supplies",
|
|
92752
|
+
// the table should agree. Conservative — only lowercase ASCII tokens
|
|
92753
|
+
// joined by underscores (≤ 40 chars) qualify, so IDs/paths/emails/
|
|
92754
|
+
// slugs/camelCase pass through verbatim.
|
|
92755
|
+
const SNAKE_CASE_CATEGORY_RE = /^[a-z]+(_[a-z]+)+$/;
|
|
92756
|
+
function prettifyCategoryCell(value) {
|
|
92757
|
+
if (typeof value !== 'string') return String(value !== null && value !== void 0 ? value : '');
|
|
92758
|
+
if (value.length > 40) return value;
|
|
92759
|
+
if (!SNAKE_CASE_CATEGORY_RE.test(value)) return value;
|
|
92760
|
+
return prettifyLabel(value);
|
|
92761
|
+
}
|
|
92762
|
+
|
|
92763
|
+
// Numeric string under a money column, e.g. "81,200" / "105444" / "-1,250.50".
|
|
92764
|
+
// Markdown tables (the common concierge path) arrive as text cells, so a
|
|
92765
|
+
// salary value is the STRING "81,200" — not a number — and skips the
|
|
92766
|
+
// USD_FORMATTER branch below. We prefix "$" while keeping the source's own
|
|
92767
|
+
// grouping/decimals so it reads "$81,200" (not "$81,200.00"). A leading "$"
|
|
92768
|
+
// already present makes the regex miss, so we never double-prefix.
|
|
92769
|
+
const _NUMERIC_STRING_RE = /^-?\d[\d,]*(\.\d+)?$/;
|
|
92586
92770
|
function formatCell(v, columnKey) {
|
|
92587
92771
|
if (v == null) return '';
|
|
92588
92772
|
if (typeof v === 'number') {
|
|
@@ -92592,7 +92776,17 @@
|
|
|
92592
92776
|
return Number.isInteger(v) ? String(v) : v.toFixed(2);
|
|
92593
92777
|
}
|
|
92594
92778
|
if (typeof v === 'object') return JSON.stringify(v);
|
|
92595
|
-
|
|
92779
|
+
// Numeric string under a money-named column → prefix "$" (Bug: concierge
|
|
92780
|
+
// salary table showed "81,200" with no currency symbol).
|
|
92781
|
+
if (isCurrencyKey(columnKey) && _NUMERIC_STRING_RE.test(String(v).trim())) {
|
|
92782
|
+
return "$".concat(String(v).trim());
|
|
92783
|
+
}
|
|
92784
|
+
// String cells: prettify snake_case category labels (e.g.
|
|
92785
|
+
// `office_supplies` → "Office Supplies") so the table reads
|
|
92786
|
+
// consistently with the chart's X-axis ticks on the same data.
|
|
92787
|
+
// Conservative: anything not a clean lowercase_underscore token is
|
|
92788
|
+
// returned verbatim, so paths/IDs/emails/slugs are preserved.
|
|
92789
|
+
return prettifyCategoryCell(v);
|
|
92596
92790
|
}
|
|
92597
92791
|
|
|
92598
92792
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -93626,8 +93820,22 @@
|
|
|
93626
93820
|
// When streaming, hide trailing table lines that haven't formed a complete
|
|
93627
93821
|
// table yet (no separator row). Without this, partial `| col |` lines flash
|
|
93628
93822
|
// as raw pipe text before ReactMarkdown can recognise the block as a table.
|
|
93823
|
+
// Also hides incomplete A2UI fences (```eshal-ui / ```eshal-chart / ```chart)
|
|
93824
|
+
// that haven't received their closing ``` yet — prevents raw JSON flash.
|
|
93629
93825
|
function sanitizeStreamingContent(text, isStreaming) {
|
|
93630
93826
|
if (!isStreaming) return text;
|
|
93827
|
+
|
|
93828
|
+
// A2UI fence guard: if an opening fence exists without a matching closing
|
|
93829
|
+
// ```, suppress everything from that opening onwards so the partial JSON/
|
|
93830
|
+
// code never flashes as raw text in the bubble.
|
|
93831
|
+
const openMatch = /^```(?:eshal-ui|eshal-chart|chart)\b/m.exec(text);
|
|
93832
|
+
if (openMatch) {
|
|
93833
|
+
const rest = text.slice(openMatch.index + openMatch[0].length);
|
|
93834
|
+
const hasClose = rest.split('\n').some(l => l.trim() === '```');
|
|
93835
|
+
if (!hasClose) {
|
|
93836
|
+
return text.slice(0, openMatch.index).trimEnd();
|
|
93837
|
+
}
|
|
93838
|
+
}
|
|
93631
93839
|
const lines = text.split('\n');
|
|
93632
93840
|
// Find last non-empty line index
|
|
93633
93841
|
let last = lines.length - 1;
|