@eshal-bot/chat-widget 0.1.40 → 0.1.42
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 +5 -5
- package/dist/chat-widget.js +301 -190
- package/dist/chat-widget.min.js +6 -6
- package/dist/chat-widget.umd.js +5 -5
- package/package.json +1 -1
- package/dist/chat-widget.esm.js.map +0 -1
- package/dist/chat-widget.js.map +0 -1
- package/dist/chat-widget.umd.js.map +0 -1
package/dist/chat-widget.js
CHANGED
|
@@ -8582,31 +8582,34 @@
|
|
|
8582
8582
|
};
|
|
8583
8583
|
|
|
8584
8584
|
/**
|
|
8585
|
-
* Constructs the WebSocket URL
|
|
8586
|
-
*
|
|
8587
|
-
*
|
|
8588
|
-
*
|
|
8589
|
-
*
|
|
8585
|
+
* Constructs the voice WebSocket base URL — ALWAYS derived from the API
|
|
8586
|
+
* base URL the widget is embedded with, as `wss://{host}/voice/ws`.
|
|
8587
|
+
*
|
|
8588
|
+
* `/voice/ws/{org}/{session}` is terminated by the frontend custom server
|
|
8589
|
+
* (packages/frontend/server.js), which proxies it in-cluster to
|
|
8590
|
+
* knowledge-api on `/ws`. The widget always talks to the same origin it
|
|
8591
|
+
* loaded its config from, so the WS endpoint is fully determined by
|
|
8592
|
+
* `apiBaseUrl`. There is intentionally NO override parameter: voice can
|
|
8593
|
+
* never be pointed at the (no-longer-public) knowledge-api LoadBalancer by
|
|
8594
|
+
* a stale embed option or backend config value.
|
|
8595
|
+
*
|
|
8596
|
+
* @param {string} apiBaseUrl - Frontend base URL, e.g. https://demo.eshal.ai
|
|
8597
|
+
* @returns {string} Voice WS base, e.g. wss://demo.eshal.ai/voice/ws
|
|
8590
8598
|
*/
|
|
8591
|
-
const getWebSocketUrl =
|
|
8592
|
-
if (wsBaseUrl) {
|
|
8593
|
-
return wsBaseUrl.replace(/\/$/, '');
|
|
8594
|
-
}
|
|
8599
|
+
const getWebSocketUrl = apiBaseUrl => {
|
|
8595
8600
|
if (!apiBaseUrl) {
|
|
8596
|
-
//
|
|
8601
|
+
// No base URL yet (config still loading) — safe public default.
|
|
8597
8602
|
return 'wss://dev.eshal.ai/voice/ws';
|
|
8598
8603
|
}
|
|
8599
8604
|
try {
|
|
8600
8605
|
const url = new URL(apiBaseUrl);
|
|
8601
|
-
//
|
|
8606
|
+
// http→ws, https→wss; point at the frontend voice-proxy path.
|
|
8602
8607
|
url.protocol = url.protocol.replace(/^http/, 'ws');
|
|
8603
|
-
// Append the WebSocket specific path
|
|
8604
8608
|
url.pathname = '/voice/ws';
|
|
8605
|
-
//
|
|
8609
|
+
// Clean URL without a trailing slash.
|
|
8606
8610
|
return url.toString().replace(/\/$/, '');
|
|
8607
8611
|
} catch (error) {
|
|
8608
8612
|
console.error("Invalid API base URL provided for WebSocket:", error);
|
|
8609
|
-
// Fallback to default if URL parsing fails
|
|
8610
8613
|
return 'wss://dev.eshal.ai/voice/ws';
|
|
8611
8614
|
}
|
|
8612
8615
|
};
|
|
@@ -8664,94 +8667,102 @@
|
|
|
8664
8667
|
return (_ONBOARDING_FIELD_ORD = ONBOARDING_FIELD_ORDER[key]) !== null && _ONBOARDING_FIELD_ORD !== void 0 ? _ONBOARDING_FIELD_ORD : 99;
|
|
8665
8668
|
};
|
|
8666
8669
|
|
|
8667
|
-
const
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
}, config.apiKey && {
|
|
8676
|
-
Authorization: "Bearer ".concat(config.apiKey)
|
|
8677
|
-
}),
|
|
8678
|
-
body: JSON.stringify({
|
|
8679
|
-
userId: config.userId,
|
|
8680
|
-
userName: config.userName,
|
|
8681
|
-
userEmail: config.userEmail
|
|
8682
|
-
}),
|
|
8683
|
-
credentials: "include"
|
|
8684
|
-
});
|
|
8685
|
-
const data = await response.json();
|
|
8686
|
-
return data.sessionId;
|
|
8687
|
-
} catch (error) {
|
|
8688
|
-
console.error("Session creation error:", error);
|
|
8689
|
-
return null;
|
|
8690
|
-
}
|
|
8691
|
-
}
|
|
8692
|
-
return null;
|
|
8693
|
-
};
|
|
8670
|
+
const SESSION_KEY$1 = 'eshal_chat_session';
|
|
8671
|
+
const SESSION_MSG_KEY = 'eshal_chat_session_messages';
|
|
8672
|
+
|
|
8673
|
+
// Defaults used when the deploy-agent payload hasn't resolved yet (first paint
|
|
8674
|
+
// race) or doesn't include `messageLimits` (older backend). The authoritative
|
|
8675
|
+
// values come from the backend via `WIDGET_CONFIG.MESSAGE_LIMITS`.
|
|
8676
|
+
const DEFAULT_MESSAGE_CAP = 200;
|
|
8677
|
+
const DEFAULT_MESSAGE_TRIM_BATCH = 50;
|
|
8694
8678
|
|
|
8695
8679
|
/**
|
|
8696
|
-
*
|
|
8697
|
-
*
|
|
8698
|
-
*
|
|
8699
|
-
*
|
|
8700
|
-
*
|
|
8701
|
-
*/
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8680
|
+
* Sliding-window trim. Once the transcript exceeds `cap`, slice off the
|
|
8681
|
+
* oldest `trimBatch` messages — when both surfaces are at steady state the
|
|
8682
|
+
* loop runs at most once; the `while` is defensive against bursts (e.g. a
|
|
8683
|
+
* voice turn that lands several transcription chunks in one tick) that
|
|
8684
|
+
* push length far past the cap in a single render.
|
|
8685
|
+
*/
|
|
8686
|
+
function applyMessageCap(messages) {
|
|
8687
|
+
let cap = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : DEFAULT_MESSAGE_CAP;
|
|
8688
|
+
let trimBatch = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : DEFAULT_MESSAGE_TRIM_BATCH;
|
|
8689
|
+
if (!Array.isArray(messages)) return [];
|
|
8690
|
+
if (typeof cap !== 'number' || cap <= 0) cap = DEFAULT_MESSAGE_CAP;
|
|
8691
|
+
if (typeof trimBatch !== 'number' || trimBatch <= 0 || trimBatch >= cap) {
|
|
8692
|
+
trimBatch = Math.min(DEFAULT_MESSAGE_TRIM_BATCH, cap - 1);
|
|
8693
|
+
}
|
|
8694
|
+
let m = messages;
|
|
8695
|
+
while (m.length > cap) m = m.slice(trimBatch);
|
|
8696
|
+
return m;
|
|
8697
|
+
}
|
|
8698
|
+
|
|
8699
|
+
// Strip transient fields (in-flight streaming flags, Date objects) so the
|
|
8700
|
+
// persisted shape is stable across React re-mounts. Keep the bits the UI
|
|
8701
|
+
// needs to render a faithful replay: role + content + timestamp + per-message
|
|
8702
|
+
// metadata (sources, feedback, prompt suggestions, content type).
|
|
8703
|
+
//
|
|
8704
|
+
// The welcome message is INTENTIONALLY excluded. It's client-side display
|
|
8705
|
+
// state generated from `widgetConfig.welcomeMessage` on every mount; the
|
|
8706
|
+
// hydrate path always prepends a fresh welcome above the restored history.
|
|
8707
|
+
// Persisting it would cause a duplicate-welcome render on reload because the
|
|
8708
|
+
// stored copy lacks the `isWelcome` flag (sanitize would strip it) and gets
|
|
8709
|
+
// rendered as a regular assistant message alongside the freshly-added one.
|
|
8710
|
+
function sanitizeMessagesForStorage(messages) {
|
|
8711
|
+
if (!Array.isArray(messages)) return [];
|
|
8712
|
+
return messages.filter(m => m && !m.isWelcome && (m.content || Array.isArray(m.sources) && m.sources.length > 0)).map(m => {
|
|
8713
|
+
var _m$type;
|
|
8714
|
+
return _objectSpread2(_objectSpread2(_objectSpread2({
|
|
8715
|
+
id: m.id,
|
|
8716
|
+
role: m.role,
|
|
8717
|
+
content: m.content,
|
|
8718
|
+
timestamp: m.timestamp instanceof Date ? m.timestamp.toISOString() : typeof m.timestamp === 'string' ? m.timestamp : new Date().toISOString(),
|
|
8719
|
+
type: (_m$type = m.type) !== null && _m$type !== void 0 ? _m$type : null
|
|
8720
|
+
}, Array.isArray(m.sources) && m.sources.length > 0 ? {
|
|
8721
|
+
sources: m.sources
|
|
8722
|
+
} : {}), m.feedback !== undefined ? {
|
|
8723
|
+
feedback: m.feedback
|
|
8724
|
+
} : {}), Array.isArray(m.prompts) && m.prompts.length > 0 ? {
|
|
8725
|
+
prompts: m.prompts
|
|
8726
|
+
} : {});
|
|
8713
8727
|
});
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8728
|
+
}
|
|
8729
|
+
const saveSessionMessages = (orgId, conversationId, messages, limits) => {
|
|
8730
|
+
if (!orgId || !conversationId) return;
|
|
8731
|
+
try {
|
|
8732
|
+
const cap = limits === null || limits === void 0 ? void 0 : limits.cap;
|
|
8733
|
+
const trimBatch = limits === null || limits === void 0 ? void 0 : limits.trimBatch;
|
|
8734
|
+
const trimmed = applyMessageCap(messages, cap, trimBatch);
|
|
8735
|
+
const sanitized = sanitizeMessagesForStorage(trimmed);
|
|
8736
|
+
localStorage.setItem("".concat(SESSION_MSG_KEY, "_").concat(orgId, "_").concat(conversationId), JSON.stringify(sanitized));
|
|
8737
|
+
} catch (_unused) {
|
|
8738
|
+
// Quota / private-mode / serialization failure — drop silently. The
|
|
8739
|
+
// user-visible chat is unaffected; only the post-reload replay is lost.
|
|
8719
8740
|
}
|
|
8720
|
-
const data = await response.json();
|
|
8721
|
-
return data.messages || [];
|
|
8722
8741
|
};
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8742
|
+
const getSessionMessages = (orgId, conversationId) => {
|
|
8743
|
+
if (!orgId || !conversationId) return [];
|
|
8744
|
+
try {
|
|
8745
|
+
const raw = localStorage.getItem("".concat(SESSION_MSG_KEY, "_").concat(orgId, "_").concat(conversationId));
|
|
8746
|
+
if (!raw) return [];
|
|
8747
|
+
const parsed = JSON.parse(raw);
|
|
8748
|
+
if (!Array.isArray(parsed)) return [];
|
|
8749
|
+
// Restore Date objects so MessageList timestamp formatting works without
|
|
8750
|
+
// every downstream consumer doing the conversion itself.
|
|
8751
|
+
return parsed.map(m => _objectSpread2(_objectSpread2({}, m), {}, {
|
|
8752
|
+
timestamp: m.timestamp ? new Date(m.timestamp) : new Date()
|
|
8753
|
+
}));
|
|
8754
|
+
} catch (_unused2) {
|
|
8755
|
+
return [];
|
|
8733
8756
|
}
|
|
8757
|
+
};
|
|
8758
|
+
const clearSessionMessages = (orgId, conversationId) => {
|
|
8759
|
+
if (!orgId || !conversationId) return;
|
|
8734
8760
|
try {
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
headers: {
|
|
8739
|
-
"Content-Type": "application/json"
|
|
8740
|
-
},
|
|
8741
|
-
credentials: "include"
|
|
8742
|
-
});
|
|
8743
|
-
if (!response.ok) {
|
|
8744
|
-
throw new Error("HTTP error! status: ".concat(response.status));
|
|
8745
|
-
}
|
|
8746
|
-
const data = await response.json();
|
|
8747
|
-
return data;
|
|
8748
|
-
} catch (error) {
|
|
8749
|
-
console.error("Failed to fetch agent configuration:", error);
|
|
8750
|
-
throw error;
|
|
8761
|
+
localStorage.removeItem("".concat(SESSION_MSG_KEY, "_").concat(orgId, "_").concat(conversationId));
|
|
8762
|
+
} catch (_unused3) {
|
|
8763
|
+
// ignore
|
|
8751
8764
|
}
|
|
8752
8765
|
};
|
|
8753
|
-
|
|
8754
|
-
const SESSION_KEY$1 = 'eshal_chat_session';
|
|
8755
8766
|
const getTimeoutMs = (value, unit) => {
|
|
8756
8767
|
if (value === undefined || value === null || !unit) return null;
|
|
8757
8768
|
const multipliers = {
|
|
@@ -8772,7 +8783,7 @@
|
|
|
8772
8783
|
const raw = localStorage.getItem("".concat(SESSION_KEY$1, "_").concat(orgId));
|
|
8773
8784
|
if (!raw) return null;
|
|
8774
8785
|
return JSON.parse(raw);
|
|
8775
|
-
} catch (
|
|
8786
|
+
} catch (_unused4) {
|
|
8776
8787
|
return null;
|
|
8777
8788
|
}
|
|
8778
8789
|
};
|
|
@@ -8822,7 +8833,7 @@
|
|
|
8822
8833
|
createdAt: now,
|
|
8823
8834
|
firstInteractionAt: null
|
|
8824
8835
|
}, extra)));
|
|
8825
|
-
} catch (
|
|
8836
|
+
} catch (_unused5) {}
|
|
8826
8837
|
};
|
|
8827
8838
|
const updateActivity = orgId => {
|
|
8828
8839
|
try {
|
|
@@ -8836,7 +8847,7 @@
|
|
|
8836
8847
|
session.firstInteractionAt = now;
|
|
8837
8848
|
}
|
|
8838
8849
|
localStorage.setItem("".concat(SESSION_KEY$1, "_").concat(orgId), JSON.stringify(session));
|
|
8839
|
-
} catch (
|
|
8850
|
+
} catch (_unused6) {}
|
|
8840
8851
|
};
|
|
8841
8852
|
const markOnboardingCompleted = orgId => {
|
|
8842
8853
|
try {
|
|
@@ -8844,7 +8855,7 @@
|
|
|
8844
8855
|
if (!session) return;
|
|
8845
8856
|
session.onboardingCompleted = true;
|
|
8846
8857
|
localStorage.setItem("".concat(SESSION_KEY$1, "_").concat(orgId), JSON.stringify(session));
|
|
8847
|
-
} catch (
|
|
8858
|
+
} catch (_unused7) {}
|
|
8848
8859
|
};
|
|
8849
8860
|
const markCsatSubmitted = orgId => {
|
|
8850
8861
|
try {
|
|
@@ -8852,12 +8863,21 @@
|
|
|
8852
8863
|
if (!session) return;
|
|
8853
8864
|
session.csatSubmitted = true;
|
|
8854
8865
|
localStorage.setItem("".concat(SESSION_KEY$1, "_").concat(orgId), JSON.stringify(session));
|
|
8855
|
-
} catch (
|
|
8866
|
+
} catch (_unused8) {}
|
|
8856
8867
|
};
|
|
8857
8868
|
const clearSession$2 = orgId => {
|
|
8858
8869
|
try {
|
|
8870
|
+
// Read the session BEFORE removing it so we know which messages bucket to
|
|
8871
|
+
// clear. Inactivity sweepers / explicit resets call this path; leaving the
|
|
8872
|
+
// messages bucket behind would resurrect the prior transcript when a new
|
|
8873
|
+
// session happens to land on the same conversationId (extremely rare, but
|
|
8874
|
+
// free to defend against).
|
|
8875
|
+
const existing = getSession(orgId);
|
|
8859
8876
|
localStorage.removeItem("".concat(SESSION_KEY$1, "_").concat(orgId));
|
|
8860
|
-
|
|
8877
|
+
if (existing !== null && existing !== void 0 && existing.conversationId) {
|
|
8878
|
+
clearSessionMessages(orgId, existing.conversationId);
|
|
8879
|
+
}
|
|
8880
|
+
} catch (_unused9) {}
|
|
8861
8881
|
};
|
|
8862
8882
|
const savePromptSuggestions = (orgId, prompts) => {
|
|
8863
8883
|
try {
|
|
@@ -8867,14 +8887,14 @@
|
|
|
8867
8887
|
return;
|
|
8868
8888
|
}
|
|
8869
8889
|
localStorage.setItem("".concat(SESSION_KEY$1, "_prompts_").concat(orgId), JSON.stringify(prompts));
|
|
8870
|
-
} catch (
|
|
8890
|
+
} catch (_unused0) {}
|
|
8871
8891
|
};
|
|
8872
8892
|
const getPromptSuggestions = orgId => {
|
|
8873
8893
|
try {
|
|
8874
8894
|
const raw = localStorage.getItem("".concat(SESSION_KEY$1, "_prompts_").concat(orgId));
|
|
8875
8895
|
if (!raw) return [];
|
|
8876
8896
|
return JSON.parse(raw) || [];
|
|
8877
|
-
} catch (
|
|
8897
|
+
} catch (_unused1) {
|
|
8878
8898
|
return [];
|
|
8879
8899
|
}
|
|
8880
8900
|
};
|
|
@@ -8909,7 +8929,6 @@
|
|
|
8909
8929
|
welcomeMessage,
|
|
8910
8930
|
quickQuestions = [],
|
|
8911
8931
|
apiBaseUrl,
|
|
8912
|
-
wsBaseUrl,
|
|
8913
8932
|
apiKey,
|
|
8914
8933
|
organizationId,
|
|
8915
8934
|
autoOpen,
|
|
@@ -8920,8 +8939,17 @@
|
|
|
8920
8939
|
onboardingEnabled = false,
|
|
8921
8940
|
collectionPrompt,
|
|
8922
8941
|
inactivityTimeoutValue,
|
|
8923
|
-
inactivityTimeoutUnit
|
|
8942
|
+
inactivityTimeoutUnit,
|
|
8943
|
+
// Sliding-window limits for the persisted transcript. Sourced from the
|
|
8944
|
+
// backend's `messageLimits` field on the deploy-agent payload so per-env
|
|
8945
|
+
// configuration ships through one channel. Falls back to module defaults
|
|
8946
|
+
// during the brief mount window before the deploy-agent fetch resolves.
|
|
8947
|
+
messageLimits
|
|
8924
8948
|
} = _ref2;
|
|
8949
|
+
const resolvedMessageLimits = reactExports.useMemo(() => ({
|
|
8950
|
+
cap: typeof (messageLimits === null || messageLimits === void 0 ? void 0 : messageLimits.cap) === 'number' && messageLimits.cap > 0 ? messageLimits.cap : DEFAULT_MESSAGE_CAP,
|
|
8951
|
+
trimBatch: typeof (messageLimits === null || messageLimits === void 0 ? void 0 : messageLimits.trimBatch) === 'number' && messageLimits.trimBatch > 0 ? messageLimits.trimBatch : DEFAULT_MESSAGE_TRIM_BATCH
|
|
8952
|
+
}), [messageLimits === null || messageLimits === void 0 ? void 0 : messageLimits.cap, messageLimits === null || messageLimits === void 0 ? void 0 : messageLimits.trimBatch]);
|
|
8925
8953
|
const [isOpen, setIsOpen] = reactExports.useState(autoOpen);
|
|
8926
8954
|
const [isMinimized, setIsMinimized] = reactExports.useState(false);
|
|
8927
8955
|
const [isDark, setIsDark] = reactExports.useState(darkMode);
|
|
@@ -9051,8 +9079,9 @@
|
|
|
9051
9079
|
const visitorProfileRef = reactExports.useRef(null);
|
|
9052
9080
|
const websocketUrl = reactExports.useMemo(() => {
|
|
9053
9081
|
if (!organizationId) return null;
|
|
9054
|
-
//
|
|
9055
|
-
|
|
9082
|
+
// Voice WS base is always derived from apiBaseUrl (the frontend origin
|
|
9083
|
+
// the widget is embedded with) → wss://{host}/voice/ws. No override.
|
|
9084
|
+
const resolvedWsBaseUrl = getWebSocketUrl(apiBaseUrl);
|
|
9056
9085
|
return "".concat(resolvedWsBaseUrl, "/").concat(organizationId, "/").concat(bidiSessionId);
|
|
9057
9086
|
}, [apiBaseUrl, organizationId, bidiSessionId]);
|
|
9058
9087
|
const getNextMessageId = reactExports.useCallback(() => {
|
|
@@ -9140,91 +9169,62 @@
|
|
|
9140
9169
|
return;
|
|
9141
9170
|
}
|
|
9142
9171
|
|
|
9143
|
-
// Restore session —
|
|
9144
|
-
|
|
9172
|
+
// Restore session — hydrate from localStorage only. We no longer hit
|
|
9173
|
+
// `GET /api/v1/conversations/:orgId/:conversationId` because orgs with
|
|
9174
|
+
// the Eshal Inbox conversation route turned OFF never get rows written
|
|
9175
|
+
// (`isInboxRouteEnabled` gates persistence) and the empty backend reply
|
|
9176
|
+
// was wiping the on-screen transcript on every reload. localStorage is
|
|
9177
|
+
// now the only source of session replay; inbox-on orgs still see their
|
|
9178
|
+
// full transcript because every settled message is mirrored locally.
|
|
9179
|
+
if (!historyLoadedRef.current) {
|
|
9145
9180
|
historyLoadedRef.current = true;
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
const
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
return {
|
|
9165
|
-
source_type: (_ref3 = (_s$source_type = s.source_type) !== null && _s$source_type !== void 0 ? _s$source_type : s.sourceType) !== null && _ref3 !== void 0 ? _ref3 : 'website',
|
|
9166
|
-
source_id: (_s$source_id = s.source_id) !== null && _s$source_id !== void 0 ? _s$source_id : s.sourceid,
|
|
9167
|
-
source_name: (_ref4 = (_s$source_name = s.source_name) !== null && _s$source_name !== void 0 ? _s$source_name : s.sourceName) !== null && _ref4 !== void 0 ? _ref4 : '',
|
|
9168
|
-
url: s.url
|
|
9169
|
-
};
|
|
9170
|
-
}).filter(s => s.source_id || s.url) : [];
|
|
9171
|
-
return _objectSpread2(_objectSpread2(_objectSpread2({}, createMessage({
|
|
9172
|
-
id: msg.id || "history-".concat(index, "-").concat(Date.now()),
|
|
9173
|
-
role: msg.role || (msg.sender === 'user' ? 'user' : 'assistant'),
|
|
9174
|
-
content: msg.message || msg.content || ''
|
|
9175
|
-
})), {}, {
|
|
9176
|
-
timestamp: msg.time ? new Date(msg.time) : new Date()
|
|
9177
|
-
}, Array.isArray(msg.prompts) && msg.prompts.length > 0 ? {
|
|
9178
|
-
prompts: msg.prompts
|
|
9179
|
-
} : {}), sources.length > 0 ? {
|
|
9180
|
-
sources
|
|
9181
|
-
} : {});
|
|
9181
|
+
const stored = getSessionMessages(organizationId, bidiSessionId);
|
|
9182
|
+
if (stored.length > 0) {
|
|
9183
|
+
historyHasMessagesRef.current = true;
|
|
9184
|
+
// Prepend the welcome message so reloads still lead with it,
|
|
9185
|
+
// matching the pre-localStorage replay behaviour. The welcome
|
|
9186
|
+
// message is client-only, never written to the messages bucket,
|
|
9187
|
+
// so it needs to be reattached on every hydrate.
|
|
9188
|
+
if (welcomeMessage) {
|
|
9189
|
+
var _stored$;
|
|
9190
|
+
const firstTs = (_stored$ = stored[0]) === null || _stored$ === void 0 ? void 0 : _stored$.timestamp;
|
|
9191
|
+
const firstTsMs = firstTs instanceof Date ? firstTs.getTime() : firstTs ? new Date(firstTs).getTime() : Date.now();
|
|
9192
|
+
const welcomeMsg = _objectSpread2(_objectSpread2({}, createMessage({
|
|
9193
|
+
id: 'welcome-restored',
|
|
9194
|
+
role: 'assistant',
|
|
9195
|
+
content: welcomeMessage
|
|
9196
|
+
})), {}, {
|
|
9197
|
+
isWelcome: true,
|
|
9198
|
+
timestamp: new Date(firstTsMs - 1)
|
|
9182
9199
|
});
|
|
9200
|
+
setMessages([welcomeMsg, ...stored]);
|
|
9201
|
+
} else {
|
|
9202
|
+
setMessages(stored);
|
|
9203
|
+
}
|
|
9183
9204
|
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9205
|
+
// If no message in the restored transcript carries prompt
|
|
9206
|
+
// suggestions, fall back to the legacy per-org bucket. Preserves
|
|
9207
|
+
// the "prompts survive refresh" UX even when the persistence layer
|
|
9208
|
+
// hasn't seen an assistant turn yet.
|
|
9209
|
+
const hasAnyPrompts = stored.some(m => m.role === 'assistant' && Array.isArray(m.prompts) && m.prompts.length > 0);
|
|
9210
|
+
if (!hasAnyPrompts) {
|
|
9211
|
+
const savedPrompts = getPromptSuggestions(organizationId);
|
|
9212
|
+
if (savedPrompts.length > 0) {
|
|
9213
|
+
setMessages(prev => {
|
|
9214
|
+
const next = [...prev];
|
|
9215
|
+
for (let i = next.length - 1; i >= 0; i -= 1) {
|
|
9216
|
+
if (next[i].role === 'assistant') {
|
|
9217
|
+
next[i] = _objectSpread2(_objectSpread2({}, next[i]), {}, {
|
|
9193
9218
|
prompts: savedPrompts
|
|
9194
9219
|
});
|
|
9195
9220
|
break;
|
|
9196
9221
|
}
|
|
9197
9222
|
}
|
|
9198
|
-
|
|
9199
|
-
}
|
|
9200
|
-
|
|
9201
|
-
// Always prepend the welcome message so it shows at the top after refresh,
|
|
9202
|
-
// matching chatbot-preview behaviour (welcome message is client-side only
|
|
9203
|
-
// and is not stored in server history).
|
|
9204
|
-
// Use a timestamp before the first history message so it sorts first
|
|
9205
|
-
// (activeMessages is sorted by timestamp before being passed to the UI).
|
|
9206
|
-
if (welcomeMessage) {
|
|
9207
|
-
var _historyMessages$;
|
|
9208
|
-
const firstTs = (_historyMessages$ = historyMessages[0]) === null || _historyMessages$ === void 0 ? void 0 : _historyMessages$.timestamp;
|
|
9209
|
-
const firstTsMs = firstTs instanceof Date ? firstTs.getTime() : firstTs ? new Date(firstTs).getTime() : Date.now();
|
|
9210
|
-
const welcomeMsg = _objectSpread2(_objectSpread2({}, createMessage({
|
|
9211
|
-
id: 'welcome-restored',
|
|
9212
|
-
role: 'assistant',
|
|
9213
|
-
content: welcomeMessage
|
|
9214
|
-
})), {}, {
|
|
9215
|
-
isWelcome: true,
|
|
9216
|
-
timestamp: new Date(firstTsMs - 1)
|
|
9223
|
+
return next;
|
|
9217
9224
|
});
|
|
9218
|
-
setMessages([welcomeMsg, ...historyMessages]);
|
|
9219
|
-
} else {
|
|
9220
|
-
setMessages(historyMessages);
|
|
9221
9225
|
}
|
|
9222
9226
|
}
|
|
9223
|
-
}
|
|
9224
|
-
console.error('[Session] History fetch failed:', err);
|
|
9225
|
-
}).finally(() => {
|
|
9226
|
-
setIsConversationLoading(false);
|
|
9227
|
-
});
|
|
9227
|
+
}
|
|
9228
9228
|
}
|
|
9229
9229
|
} else {
|
|
9230
9230
|
// Session is expired, invalid, or non-existent
|
|
@@ -9236,9 +9236,27 @@
|
|
|
9236
9236
|
if (existing && bidiSessionId === existing.conversationId) {
|
|
9237
9237
|
const newId = "widget-session-".concat(Math.random().toString(36).slice(2, 9));
|
|
9238
9238
|
console.warn("[Session] Rotating expired ID ".concat(bidiSessionId, " -> ").concat(newId));
|
|
9239
|
+
// Drop the expired conversation's localStorage transcript before
|
|
9240
|
+
// rotating — otherwise the bucket sticks around forever (the key
|
|
9241
|
+
// includes the conversationId, so it'd never get hit again but
|
|
9242
|
+
// would still occupy origin storage on the customer's site).
|
|
9243
|
+
clearSessionMessages(organizationId, bidiSessionId);
|
|
9239
9244
|
setBidiSessionId(newId);
|
|
9240
9245
|
// Note: saveSession will be called on the next run with the newId
|
|
9241
9246
|
} else {
|
|
9247
|
+
// Cold-start cleanup: the `useState` initializer above generates a
|
|
9248
|
+
// FRESH `bidiSessionId` when it can't reuse the stored session (timeout
|
|
9249
|
+
// expired before reload, settings snapshot mismatched, or the deploy-
|
|
9250
|
+
// agent payload was still loading at first paint so it couldn't run
|
|
9251
|
+
// `isSessionValid`). In that case `existing.conversationId` points to
|
|
9252
|
+
// a now-stale conversation whose messages bucket would otherwise be
|
|
9253
|
+
// orphaned forever — `clearSession` runs in `resetConversation` (which
|
|
9254
|
+
// we never hit here) and the rotate-id branch above only fires when
|
|
9255
|
+
// the IDs match. Mirror the frontend `useSessionManager` hydrate
|
|
9256
|
+
// effect: clean up the stale bucket before we save the new session.
|
|
9257
|
+
if (existing !== null && existing !== void 0 && existing.conversationId && existing.conversationId !== bidiSessionId) {
|
|
9258
|
+
clearSessionMessages(organizationId, existing.conversationId);
|
|
9259
|
+
}
|
|
9242
9260
|
// We have a fresh ID (either from initializer or rotation), persist it if not already there
|
|
9243
9261
|
if (!existing || existing.conversationId !== bidiSessionId) {
|
|
9244
9262
|
saveSession(organizationId, bidiSessionId, {
|
|
@@ -9264,6 +9282,29 @@
|
|
|
9264
9282
|
}
|
|
9265
9283
|
}, [messages, organizationId]);
|
|
9266
9284
|
|
|
9285
|
+
// ── Sliding-window trim: drop the oldest TRIM_BATCH messages when state
|
|
9286
|
+
// exceeds CAP. Runs only when no message is mid-stream so we never slice
|
|
9287
|
+
// through a partial assistant token. Mutates React state (matches the
|
|
9288
|
+
// intended UX — old scrollback drops in front of the user once the cap is
|
|
9289
|
+
// hit, keeping what's on screen identical to what's in localStorage).
|
|
9290
|
+
reactExports.useEffect(() => {
|
|
9291
|
+
if (isConversationLoading || isLoading) return;
|
|
9292
|
+
if (messages.length <= resolvedMessageLimits.cap) return;
|
|
9293
|
+
setMessages(prev => applyMessageCap(prev, resolvedMessageLimits.cap, resolvedMessageLimits.trimBatch));
|
|
9294
|
+
}, [messages.length, isConversationLoading, isLoading, resolvedMessageLimits.cap, resolvedMessageLimits.trimBatch]);
|
|
9295
|
+
|
|
9296
|
+
// ── Persist a settled snapshot of the transcript. We write only when
|
|
9297
|
+
// streaming/voice traffic has quiesced so a partial assistant message
|
|
9298
|
+
// never lands in localStorage — on reload the next snapshot will reflect
|
|
9299
|
+
// the completed turn. Skipped while history is hydrating (initial mount
|
|
9300
|
+
// race) so the first restore isn't immediately overwritten by an empty
|
|
9301
|
+
// pre-hydrate state.
|
|
9302
|
+
reactExports.useEffect(() => {
|
|
9303
|
+
if (!organizationId || !bidiSessionId) return;
|
|
9304
|
+
if (isConversationLoading || isLoading) return;
|
|
9305
|
+
saveSessionMessages(organizationId, bidiSessionId, messages, resolvedMessageLimits);
|
|
9306
|
+
}, [messages, organizationId, bidiSessionId, isConversationLoading, isLoading, resolvedMessageLimits]);
|
|
9307
|
+
|
|
9267
9308
|
// Show the onboarding form on first load; bypass it if already completed (persisted in session)
|
|
9268
9309
|
reactExports.useEffect(() => {
|
|
9269
9310
|
if (!onboardingEnabled || onboardingCompleted) return;
|
|
@@ -10252,11 +10293,11 @@
|
|
|
10252
10293
|
const rawSources = (_event$sources = event.sources) !== null && _event$sources !== void 0 ? _event$sources : event.Sources;
|
|
10253
10294
|
if (Array.isArray(rawSources) && rawSources.length > 0) {
|
|
10254
10295
|
const normalizedSources = rawSources.map(s => {
|
|
10255
|
-
var
|
|
10296
|
+
var _ref3, _s$source_type, _ref4, _s$source_id, _ref5, _s$source_name;
|
|
10256
10297
|
return {
|
|
10257
|
-
source_type: (
|
|
10258
|
-
source_id: (
|
|
10259
|
-
source_name: (
|
|
10298
|
+
source_type: (_ref3 = (_s$source_type = s.source_type) !== null && _s$source_type !== void 0 ? _s$source_type : s.sourceType) !== null && _ref3 !== void 0 ? _ref3 : 'file',
|
|
10299
|
+
source_id: (_ref4 = (_s$source_id = s.source_id) !== null && _s$source_id !== void 0 ? _s$source_id : s.sourceid) !== null && _ref4 !== void 0 ? _ref4 : s.sourceId,
|
|
10300
|
+
source_name: (_ref5 = (_s$source_name = s.source_name) !== null && _s$source_name !== void 0 ? _s$source_name : s.sourceName) !== null && _ref5 !== void 0 ? _ref5 : '',
|
|
10260
10301
|
url: s.url
|
|
10261
10302
|
};
|
|
10262
10303
|
}).filter(s => s.source_id || s.url);
|
|
@@ -10563,6 +10604,10 @@
|
|
|
10563
10604
|
setMessages([]);
|
|
10564
10605
|
setBidiMessages([]);
|
|
10565
10606
|
|
|
10607
|
+
// Drop the localStorage transcript for the conversation we're abandoning
|
|
10608
|
+
// BEFORE we rotate to a fresh sessionId, otherwise the old bucket leaks.
|
|
10609
|
+
clearSessionMessages(organizationId, bidiSessionId);
|
|
10610
|
+
|
|
10566
10611
|
// Reset onboarding
|
|
10567
10612
|
setOnboardingActive(false);
|
|
10568
10613
|
setOnboardingCompleted(false);
|
|
@@ -10601,7 +10646,7 @@
|
|
|
10601
10646
|
setOnboardingActive(true);
|
|
10602
10647
|
}
|
|
10603
10648
|
}
|
|
10604
|
-
}, [organizationId, welcomeMessage, onboardingEnabled, onboardingQuestions]);
|
|
10649
|
+
}, [organizationId, bidiSessionId, welcomeMessage, onboardingEnabled, onboardingQuestions, inactivityTimeoutValue, inactivityTimeoutUnit]);
|
|
10605
10650
|
|
|
10606
10651
|
// Keep a stable ref to resetConversation so the timer effect does not need to
|
|
10607
10652
|
// list it as a dependency (avoids spurious re-runs — and spurious updateActivity
|
|
@@ -31781,7 +31826,7 @@
|
|
|
31781
31826
|
if (typeof document === 'undefined') {
|
|
31782
31827
|
return null;
|
|
31783
31828
|
}
|
|
31784
|
-
if (
|
|
31829
|
+
if (document.currentScript && document.currentScript.tagName === 'SCRIPT' && 1 < 2 /* hack to trip TS' flow analysis */) {
|
|
31785
31830
|
return /** @type {any} */document.currentScript;
|
|
31786
31831
|
}
|
|
31787
31832
|
|
|
@@ -60277,13 +60322,70 @@
|
|
|
60277
60322
|
});
|
|
60278
60323
|
};
|
|
60279
60324
|
|
|
60325
|
+
const createSession = async config => {
|
|
60326
|
+
// Initialize chat session if needed
|
|
60327
|
+
if (config.sessionUrl) {
|
|
60328
|
+
try {
|
|
60329
|
+
const response = await fetch(config.sessionUrl, {
|
|
60330
|
+
method: "POST",
|
|
60331
|
+
headers: _objectSpread2({
|
|
60332
|
+
"Content-Type": "application/json"
|
|
60333
|
+
}, config.apiKey && {
|
|
60334
|
+
Authorization: "Bearer ".concat(config.apiKey)
|
|
60335
|
+
}),
|
|
60336
|
+
body: JSON.stringify({
|
|
60337
|
+
userId: config.userId,
|
|
60338
|
+
userName: config.userName,
|
|
60339
|
+
userEmail: config.userEmail
|
|
60340
|
+
}),
|
|
60341
|
+
credentials: "include"
|
|
60342
|
+
});
|
|
60343
|
+
const data = await response.json();
|
|
60344
|
+
return data.sessionId;
|
|
60345
|
+
} catch (error) {
|
|
60346
|
+
console.error("Session creation error:", error);
|
|
60347
|
+
return null;
|
|
60348
|
+
}
|
|
60349
|
+
}
|
|
60350
|
+
return null;
|
|
60351
|
+
};
|
|
60352
|
+
|
|
60353
|
+
/**
|
|
60354
|
+
* Fetches agent configuration from the deploy-agent endpoint
|
|
60355
|
+
* @param {string} apiBaseUrl - Base URL for the API
|
|
60356
|
+
* @param {string} orgId - Organization ID
|
|
60357
|
+
* @returns {Promise<object>} Agent configuration object
|
|
60358
|
+
*/
|
|
60359
|
+
const fetchAgentConfig = async (apiBaseUrl, orgId) => {
|
|
60360
|
+
if (!apiBaseUrl || !orgId) {
|
|
60361
|
+
throw new Error("apiBaseUrl and orgId are required");
|
|
60362
|
+
}
|
|
60363
|
+
try {
|
|
60364
|
+
const url = "".concat(apiBaseUrl.replace(/\/$/, ''), "/api/v1/deploy-agent/").concat(orgId);
|
|
60365
|
+
const response = await fetch(url, {
|
|
60366
|
+
method: "GET",
|
|
60367
|
+
headers: {
|
|
60368
|
+
"Content-Type": "application/json"
|
|
60369
|
+
},
|
|
60370
|
+
credentials: "include"
|
|
60371
|
+
});
|
|
60372
|
+
if (!response.ok) {
|
|
60373
|
+
throw new Error("HTTP error! status: ".concat(response.status));
|
|
60374
|
+
}
|
|
60375
|
+
const data = await response.json();
|
|
60376
|
+
return data;
|
|
60377
|
+
} catch (error) {
|
|
60378
|
+
console.error("Failed to fetch agent configuration:", error);
|
|
60379
|
+
throw error;
|
|
60380
|
+
}
|
|
60381
|
+
};
|
|
60382
|
+
|
|
60280
60383
|
const ChatWidget = _ref => {
|
|
60281
60384
|
var _agentConfig$concierg;
|
|
60282
60385
|
let {
|
|
60283
60386
|
// Required props for dynamic config
|
|
60284
60387
|
orgId,
|
|
60285
60388
|
apiBaseUrl,
|
|
60286
|
-
wsBaseUrl,
|
|
60287
60389
|
// Optional override props (will override fetched config if provided)
|
|
60288
60390
|
darkMode,
|
|
60289
60391
|
primaryColor,
|
|
@@ -60466,7 +60568,6 @@
|
|
|
60466
60568
|
welcomeMessage: "Hi! How can we help?",
|
|
60467
60569
|
quickQuestions: [],
|
|
60468
60570
|
apiBaseUrl: apiBaseUrl || "",
|
|
60469
|
-
wsBaseUrl: agentConfig === null || agentConfig === void 0 ? void 0 : agentConfig.wsBaseUrl,
|
|
60470
60571
|
apiKey: apiKey || "",
|
|
60471
60572
|
organizationId: orgId || "",
|
|
60472
60573
|
autoOpen: false,
|
|
@@ -60476,7 +60577,7 @@
|
|
|
60476
60577
|
onboardingQuestions: [],
|
|
60477
60578
|
onboardingEnabled: false,
|
|
60478
60579
|
collectionPrompt: undefined
|
|
60479
|
-
}), [apiBaseUrl,
|
|
60580
|
+
}), [apiBaseUrl, apiKey, orgId]);
|
|
60480
60581
|
|
|
60481
60582
|
// ALWAYS call hooks before any early returns (Rules of Hooks)
|
|
60482
60583
|
const {
|
|
@@ -60517,7 +60618,11 @@
|
|
|
60517
60618
|
welcomeMessage: widgetConfig.welcomeMessage,
|
|
60518
60619
|
quickQuestions: widgetConfig.quickQuestions,
|
|
60519
60620
|
apiBaseUrl,
|
|
60520
|
-
|
|
60621
|
+
// Voice WS endpoint is derived entirely from apiBaseUrl inside
|
|
60622
|
+
// useChatState (→ wss://{host}/voice/ws, the frontend voice proxy).
|
|
60623
|
+
// Nothing is forwarded/overridable here, so a stale backend or embed
|
|
60624
|
+
// wsBaseUrl can't point voice at the old (now non-public)
|
|
60625
|
+
// knowledge-api LoadBalancer.
|
|
60521
60626
|
apiKey,
|
|
60522
60627
|
organizationId: widgetConfig.organizationId,
|
|
60523
60628
|
autoOpen,
|
|
@@ -60528,7 +60633,13 @@
|
|
|
60528
60633
|
onboardingEnabled: widgetConfig.onboardingEnabled,
|
|
60529
60634
|
collectionPrompt: widgetConfig.collectionPrompt,
|
|
60530
60635
|
inactivityTimeoutValue: widgetConfig.inactivityTimeoutValue,
|
|
60531
|
-
inactivityTimeoutUnit: widgetConfig.inactivityTimeoutUnit
|
|
60636
|
+
inactivityTimeoutUnit: widgetConfig.inactivityTimeoutUnit,
|
|
60637
|
+
// Sliding-window cap for the localStorage transcript. Comes from
|
|
60638
|
+
// GET /deploy-agent/:orgId → agentConfig.messageLimits (single
|
|
60639
|
+
// source of truth across surfaces). When the field is missing
|
|
60640
|
+
// (older backend / fetch in flight) the hook falls back to its
|
|
60641
|
+
// module-level defaults so chat never blocks on this.
|
|
60642
|
+
messageLimits: agentConfig === null || agentConfig === void 0 ? void 0 : agentConfig.messageLimits
|
|
60532
60643
|
} : defaultConfig);
|
|
60533
60644
|
|
|
60534
60645
|
// Feedback handler — POST to /api/v1/support/feedback
|