@eshal-bot/chat-widget 0.1.40 → 0.1.41
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 +274 -168
- 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.js
CHANGED
|
@@ -8664,94 +8664,102 @@
|
|
|
8664
8664
|
return (_ONBOARDING_FIELD_ORD = ONBOARDING_FIELD_ORDER[key]) !== null && _ONBOARDING_FIELD_ORD !== void 0 ? _ONBOARDING_FIELD_ORD : 99;
|
|
8665
8665
|
};
|
|
8666
8666
|
|
|
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
|
-
};
|
|
8667
|
+
const SESSION_KEY$1 = 'eshal_chat_session';
|
|
8668
|
+
const SESSION_MSG_KEY = 'eshal_chat_session_messages';
|
|
8669
|
+
|
|
8670
|
+
// Defaults used when the deploy-agent payload hasn't resolved yet (first paint
|
|
8671
|
+
// race) or doesn't include `messageLimits` (older backend). The authoritative
|
|
8672
|
+
// values come from the backend via `WIDGET_CONFIG.MESSAGE_LIMITS`.
|
|
8673
|
+
const DEFAULT_MESSAGE_CAP = 200;
|
|
8674
|
+
const DEFAULT_MESSAGE_TRIM_BATCH = 50;
|
|
8694
8675
|
|
|
8695
8676
|
/**
|
|
8696
|
-
*
|
|
8697
|
-
*
|
|
8698
|
-
*
|
|
8699
|
-
*
|
|
8700
|
-
*
|
|
8701
|
-
*/
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8677
|
+
* Sliding-window trim. Once the transcript exceeds `cap`, slice off the
|
|
8678
|
+
* oldest `trimBatch` messages — when both surfaces are at steady state the
|
|
8679
|
+
* loop runs at most once; the `while` is defensive against bursts (e.g. a
|
|
8680
|
+
* voice turn that lands several transcription chunks in one tick) that
|
|
8681
|
+
* push length far past the cap in a single render.
|
|
8682
|
+
*/
|
|
8683
|
+
function applyMessageCap(messages) {
|
|
8684
|
+
let cap = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : DEFAULT_MESSAGE_CAP;
|
|
8685
|
+
let trimBatch = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : DEFAULT_MESSAGE_TRIM_BATCH;
|
|
8686
|
+
if (!Array.isArray(messages)) return [];
|
|
8687
|
+
if (typeof cap !== 'number' || cap <= 0) cap = DEFAULT_MESSAGE_CAP;
|
|
8688
|
+
if (typeof trimBatch !== 'number' || trimBatch <= 0 || trimBatch >= cap) {
|
|
8689
|
+
trimBatch = Math.min(DEFAULT_MESSAGE_TRIM_BATCH, cap - 1);
|
|
8690
|
+
}
|
|
8691
|
+
let m = messages;
|
|
8692
|
+
while (m.length > cap) m = m.slice(trimBatch);
|
|
8693
|
+
return m;
|
|
8694
|
+
}
|
|
8695
|
+
|
|
8696
|
+
// Strip transient fields (in-flight streaming flags, Date objects) so the
|
|
8697
|
+
// persisted shape is stable across React re-mounts. Keep the bits the UI
|
|
8698
|
+
// needs to render a faithful replay: role + content + timestamp + per-message
|
|
8699
|
+
// metadata (sources, feedback, prompt suggestions, content type).
|
|
8700
|
+
//
|
|
8701
|
+
// The welcome message is INTENTIONALLY excluded. It's client-side display
|
|
8702
|
+
// state generated from `widgetConfig.welcomeMessage` on every mount; the
|
|
8703
|
+
// hydrate path always prepends a fresh welcome above the restored history.
|
|
8704
|
+
// Persisting it would cause a duplicate-welcome render on reload because the
|
|
8705
|
+
// stored copy lacks the `isWelcome` flag (sanitize would strip it) and gets
|
|
8706
|
+
// rendered as a regular assistant message alongside the freshly-added one.
|
|
8707
|
+
function sanitizeMessagesForStorage(messages) {
|
|
8708
|
+
if (!Array.isArray(messages)) return [];
|
|
8709
|
+
return messages.filter(m => m && !m.isWelcome && (m.content || Array.isArray(m.sources) && m.sources.length > 0)).map(m => {
|
|
8710
|
+
var _m$type;
|
|
8711
|
+
return _objectSpread2(_objectSpread2(_objectSpread2({
|
|
8712
|
+
id: m.id,
|
|
8713
|
+
role: m.role,
|
|
8714
|
+
content: m.content,
|
|
8715
|
+
timestamp: m.timestamp instanceof Date ? m.timestamp.toISOString() : typeof m.timestamp === 'string' ? m.timestamp : new Date().toISOString(),
|
|
8716
|
+
type: (_m$type = m.type) !== null && _m$type !== void 0 ? _m$type : null
|
|
8717
|
+
}, Array.isArray(m.sources) && m.sources.length > 0 ? {
|
|
8718
|
+
sources: m.sources
|
|
8719
|
+
} : {}), m.feedback !== undefined ? {
|
|
8720
|
+
feedback: m.feedback
|
|
8721
|
+
} : {}), Array.isArray(m.prompts) && m.prompts.length > 0 ? {
|
|
8722
|
+
prompts: m.prompts
|
|
8723
|
+
} : {});
|
|
8713
8724
|
});
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8725
|
+
}
|
|
8726
|
+
const saveSessionMessages = (orgId, conversationId, messages, limits) => {
|
|
8727
|
+
if (!orgId || !conversationId) return;
|
|
8728
|
+
try {
|
|
8729
|
+
const cap = limits === null || limits === void 0 ? void 0 : limits.cap;
|
|
8730
|
+
const trimBatch = limits === null || limits === void 0 ? void 0 : limits.trimBatch;
|
|
8731
|
+
const trimmed = applyMessageCap(messages, cap, trimBatch);
|
|
8732
|
+
const sanitized = sanitizeMessagesForStorage(trimmed);
|
|
8733
|
+
localStorage.setItem("".concat(SESSION_MSG_KEY, "_").concat(orgId, "_").concat(conversationId), JSON.stringify(sanitized));
|
|
8734
|
+
} catch (_unused) {
|
|
8735
|
+
// Quota / private-mode / serialization failure — drop silently. The
|
|
8736
|
+
// user-visible chat is unaffected; only the post-reload replay is lost.
|
|
8719
8737
|
}
|
|
8720
|
-
const data = await response.json();
|
|
8721
|
-
return data.messages || [];
|
|
8722
8738
|
};
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8739
|
+
const getSessionMessages = (orgId, conversationId) => {
|
|
8740
|
+
if (!orgId || !conversationId) return [];
|
|
8741
|
+
try {
|
|
8742
|
+
const raw = localStorage.getItem("".concat(SESSION_MSG_KEY, "_").concat(orgId, "_").concat(conversationId));
|
|
8743
|
+
if (!raw) return [];
|
|
8744
|
+
const parsed = JSON.parse(raw);
|
|
8745
|
+
if (!Array.isArray(parsed)) return [];
|
|
8746
|
+
// Restore Date objects so MessageList timestamp formatting works without
|
|
8747
|
+
// every downstream consumer doing the conversion itself.
|
|
8748
|
+
return parsed.map(m => _objectSpread2(_objectSpread2({}, m), {}, {
|
|
8749
|
+
timestamp: m.timestamp ? new Date(m.timestamp) : new Date()
|
|
8750
|
+
}));
|
|
8751
|
+
} catch (_unused2) {
|
|
8752
|
+
return [];
|
|
8733
8753
|
}
|
|
8754
|
+
};
|
|
8755
|
+
const clearSessionMessages = (orgId, conversationId) => {
|
|
8756
|
+
if (!orgId || !conversationId) return;
|
|
8734
8757
|
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;
|
|
8758
|
+
localStorage.removeItem("".concat(SESSION_MSG_KEY, "_").concat(orgId, "_").concat(conversationId));
|
|
8759
|
+
} catch (_unused3) {
|
|
8760
|
+
// ignore
|
|
8751
8761
|
}
|
|
8752
8762
|
};
|
|
8753
|
-
|
|
8754
|
-
const SESSION_KEY$1 = 'eshal_chat_session';
|
|
8755
8763
|
const getTimeoutMs = (value, unit) => {
|
|
8756
8764
|
if (value === undefined || value === null || !unit) return null;
|
|
8757
8765
|
const multipliers = {
|
|
@@ -8772,7 +8780,7 @@
|
|
|
8772
8780
|
const raw = localStorage.getItem("".concat(SESSION_KEY$1, "_").concat(orgId));
|
|
8773
8781
|
if (!raw) return null;
|
|
8774
8782
|
return JSON.parse(raw);
|
|
8775
|
-
} catch (
|
|
8783
|
+
} catch (_unused4) {
|
|
8776
8784
|
return null;
|
|
8777
8785
|
}
|
|
8778
8786
|
};
|
|
@@ -8822,7 +8830,7 @@
|
|
|
8822
8830
|
createdAt: now,
|
|
8823
8831
|
firstInteractionAt: null
|
|
8824
8832
|
}, extra)));
|
|
8825
|
-
} catch (
|
|
8833
|
+
} catch (_unused5) {}
|
|
8826
8834
|
};
|
|
8827
8835
|
const updateActivity = orgId => {
|
|
8828
8836
|
try {
|
|
@@ -8836,7 +8844,7 @@
|
|
|
8836
8844
|
session.firstInteractionAt = now;
|
|
8837
8845
|
}
|
|
8838
8846
|
localStorage.setItem("".concat(SESSION_KEY$1, "_").concat(orgId), JSON.stringify(session));
|
|
8839
|
-
} catch (
|
|
8847
|
+
} catch (_unused6) {}
|
|
8840
8848
|
};
|
|
8841
8849
|
const markOnboardingCompleted = orgId => {
|
|
8842
8850
|
try {
|
|
@@ -8844,7 +8852,7 @@
|
|
|
8844
8852
|
if (!session) return;
|
|
8845
8853
|
session.onboardingCompleted = true;
|
|
8846
8854
|
localStorage.setItem("".concat(SESSION_KEY$1, "_").concat(orgId), JSON.stringify(session));
|
|
8847
|
-
} catch (
|
|
8855
|
+
} catch (_unused7) {}
|
|
8848
8856
|
};
|
|
8849
8857
|
const markCsatSubmitted = orgId => {
|
|
8850
8858
|
try {
|
|
@@ -8852,12 +8860,21 @@
|
|
|
8852
8860
|
if (!session) return;
|
|
8853
8861
|
session.csatSubmitted = true;
|
|
8854
8862
|
localStorage.setItem("".concat(SESSION_KEY$1, "_").concat(orgId), JSON.stringify(session));
|
|
8855
|
-
} catch (
|
|
8863
|
+
} catch (_unused8) {}
|
|
8856
8864
|
};
|
|
8857
8865
|
const clearSession$2 = orgId => {
|
|
8858
8866
|
try {
|
|
8867
|
+
// Read the session BEFORE removing it so we know which messages bucket to
|
|
8868
|
+
// clear. Inactivity sweepers / explicit resets call this path; leaving the
|
|
8869
|
+
// messages bucket behind would resurrect the prior transcript when a new
|
|
8870
|
+
// session happens to land on the same conversationId (extremely rare, but
|
|
8871
|
+
// free to defend against).
|
|
8872
|
+
const existing = getSession(orgId);
|
|
8859
8873
|
localStorage.removeItem("".concat(SESSION_KEY$1, "_").concat(orgId));
|
|
8860
|
-
|
|
8874
|
+
if (existing !== null && existing !== void 0 && existing.conversationId) {
|
|
8875
|
+
clearSessionMessages(orgId, existing.conversationId);
|
|
8876
|
+
}
|
|
8877
|
+
} catch (_unused9) {}
|
|
8861
8878
|
};
|
|
8862
8879
|
const savePromptSuggestions = (orgId, prompts) => {
|
|
8863
8880
|
try {
|
|
@@ -8867,14 +8884,14 @@
|
|
|
8867
8884
|
return;
|
|
8868
8885
|
}
|
|
8869
8886
|
localStorage.setItem("".concat(SESSION_KEY$1, "_prompts_").concat(orgId), JSON.stringify(prompts));
|
|
8870
|
-
} catch (
|
|
8887
|
+
} catch (_unused0) {}
|
|
8871
8888
|
};
|
|
8872
8889
|
const getPromptSuggestions = orgId => {
|
|
8873
8890
|
try {
|
|
8874
8891
|
const raw = localStorage.getItem("".concat(SESSION_KEY$1, "_prompts_").concat(orgId));
|
|
8875
8892
|
if (!raw) return [];
|
|
8876
8893
|
return JSON.parse(raw) || [];
|
|
8877
|
-
} catch (
|
|
8894
|
+
} catch (_unused1) {
|
|
8878
8895
|
return [];
|
|
8879
8896
|
}
|
|
8880
8897
|
};
|
|
@@ -8920,8 +8937,17 @@
|
|
|
8920
8937
|
onboardingEnabled = false,
|
|
8921
8938
|
collectionPrompt,
|
|
8922
8939
|
inactivityTimeoutValue,
|
|
8923
|
-
inactivityTimeoutUnit
|
|
8940
|
+
inactivityTimeoutUnit,
|
|
8941
|
+
// Sliding-window limits for the persisted transcript. Sourced from the
|
|
8942
|
+
// backend's `messageLimits` field on the deploy-agent payload so per-env
|
|
8943
|
+
// configuration ships through one channel. Falls back to module defaults
|
|
8944
|
+
// during the brief mount window before the deploy-agent fetch resolves.
|
|
8945
|
+
messageLimits
|
|
8924
8946
|
} = _ref2;
|
|
8947
|
+
const resolvedMessageLimits = reactExports.useMemo(() => ({
|
|
8948
|
+
cap: typeof (messageLimits === null || messageLimits === void 0 ? void 0 : messageLimits.cap) === 'number' && messageLimits.cap > 0 ? messageLimits.cap : DEFAULT_MESSAGE_CAP,
|
|
8949
|
+
trimBatch: typeof (messageLimits === null || messageLimits === void 0 ? void 0 : messageLimits.trimBatch) === 'number' && messageLimits.trimBatch > 0 ? messageLimits.trimBatch : DEFAULT_MESSAGE_TRIM_BATCH
|
|
8950
|
+
}), [messageLimits === null || messageLimits === void 0 ? void 0 : messageLimits.cap, messageLimits === null || messageLimits === void 0 ? void 0 : messageLimits.trimBatch]);
|
|
8925
8951
|
const [isOpen, setIsOpen] = reactExports.useState(autoOpen);
|
|
8926
8952
|
const [isMinimized, setIsMinimized] = reactExports.useState(false);
|
|
8927
8953
|
const [isDark, setIsDark] = reactExports.useState(darkMode);
|
|
@@ -9140,91 +9166,62 @@
|
|
|
9140
9166
|
return;
|
|
9141
9167
|
}
|
|
9142
9168
|
|
|
9143
|
-
// Restore session —
|
|
9144
|
-
|
|
9169
|
+
// Restore session — hydrate from localStorage only. We no longer hit
|
|
9170
|
+
// `GET /api/v1/conversations/:orgId/:conversationId` because orgs with
|
|
9171
|
+
// the Eshal Inbox conversation route turned OFF never get rows written
|
|
9172
|
+
// (`isInboxRouteEnabled` gates persistence) and the empty backend reply
|
|
9173
|
+
// was wiping the on-screen transcript on every reload. localStorage is
|
|
9174
|
+
// now the only source of session replay; inbox-on orgs still see their
|
|
9175
|
+
// full transcript because every settled message is mirrored locally.
|
|
9176
|
+
if (!historyLoadedRef.current) {
|
|
9145
9177
|
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
|
-
} : {});
|
|
9178
|
+
const stored = getSessionMessages(organizationId, bidiSessionId);
|
|
9179
|
+
if (stored.length > 0) {
|
|
9180
|
+
historyHasMessagesRef.current = true;
|
|
9181
|
+
// Prepend the welcome message so reloads still lead with it,
|
|
9182
|
+
// matching the pre-localStorage replay behaviour. The welcome
|
|
9183
|
+
// message is client-only, never written to the messages bucket,
|
|
9184
|
+
// so it needs to be reattached on every hydrate.
|
|
9185
|
+
if (welcomeMessage) {
|
|
9186
|
+
var _stored$;
|
|
9187
|
+
const firstTs = (_stored$ = stored[0]) === null || _stored$ === void 0 ? void 0 : _stored$.timestamp;
|
|
9188
|
+
const firstTsMs = firstTs instanceof Date ? firstTs.getTime() : firstTs ? new Date(firstTs).getTime() : Date.now();
|
|
9189
|
+
const welcomeMsg = _objectSpread2(_objectSpread2({}, createMessage({
|
|
9190
|
+
id: 'welcome-restored',
|
|
9191
|
+
role: 'assistant',
|
|
9192
|
+
content: welcomeMessage
|
|
9193
|
+
})), {}, {
|
|
9194
|
+
isWelcome: true,
|
|
9195
|
+
timestamp: new Date(firstTsMs - 1)
|
|
9182
9196
|
});
|
|
9197
|
+
setMessages([welcomeMsg, ...stored]);
|
|
9198
|
+
} else {
|
|
9199
|
+
setMessages(stored);
|
|
9200
|
+
}
|
|
9183
9201
|
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9202
|
+
// If no message in the restored transcript carries prompt
|
|
9203
|
+
// suggestions, fall back to the legacy per-org bucket. Preserves
|
|
9204
|
+
// the "prompts survive refresh" UX even when the persistence layer
|
|
9205
|
+
// hasn't seen an assistant turn yet.
|
|
9206
|
+
const hasAnyPrompts = stored.some(m => m.role === 'assistant' && Array.isArray(m.prompts) && m.prompts.length > 0);
|
|
9207
|
+
if (!hasAnyPrompts) {
|
|
9208
|
+
const savedPrompts = getPromptSuggestions(organizationId);
|
|
9209
|
+
if (savedPrompts.length > 0) {
|
|
9210
|
+
setMessages(prev => {
|
|
9211
|
+
const next = [...prev];
|
|
9212
|
+
for (let i = next.length - 1; i >= 0; i -= 1) {
|
|
9213
|
+
if (next[i].role === 'assistant') {
|
|
9214
|
+
next[i] = _objectSpread2(_objectSpread2({}, next[i]), {}, {
|
|
9193
9215
|
prompts: savedPrompts
|
|
9194
9216
|
});
|
|
9195
9217
|
break;
|
|
9196
9218
|
}
|
|
9197
9219
|
}
|
|
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)
|
|
9220
|
+
return next;
|
|
9217
9221
|
});
|
|
9218
|
-
setMessages([welcomeMsg, ...historyMessages]);
|
|
9219
|
-
} else {
|
|
9220
|
-
setMessages(historyMessages);
|
|
9221
9222
|
}
|
|
9222
9223
|
}
|
|
9223
|
-
}
|
|
9224
|
-
console.error('[Session] History fetch failed:', err);
|
|
9225
|
-
}).finally(() => {
|
|
9226
|
-
setIsConversationLoading(false);
|
|
9227
|
-
});
|
|
9224
|
+
}
|
|
9228
9225
|
}
|
|
9229
9226
|
} else {
|
|
9230
9227
|
// Session is expired, invalid, or non-existent
|
|
@@ -9236,9 +9233,27 @@
|
|
|
9236
9233
|
if (existing && bidiSessionId === existing.conversationId) {
|
|
9237
9234
|
const newId = "widget-session-".concat(Math.random().toString(36).slice(2, 9));
|
|
9238
9235
|
console.warn("[Session] Rotating expired ID ".concat(bidiSessionId, " -> ").concat(newId));
|
|
9236
|
+
// Drop the expired conversation's localStorage transcript before
|
|
9237
|
+
// rotating — otherwise the bucket sticks around forever (the key
|
|
9238
|
+
// includes the conversationId, so it'd never get hit again but
|
|
9239
|
+
// would still occupy origin storage on the customer's site).
|
|
9240
|
+
clearSessionMessages(organizationId, bidiSessionId);
|
|
9239
9241
|
setBidiSessionId(newId);
|
|
9240
9242
|
// Note: saveSession will be called on the next run with the newId
|
|
9241
9243
|
} else {
|
|
9244
|
+
// Cold-start cleanup: the `useState` initializer above generates a
|
|
9245
|
+
// FRESH `bidiSessionId` when it can't reuse the stored session (timeout
|
|
9246
|
+
// expired before reload, settings snapshot mismatched, or the deploy-
|
|
9247
|
+
// agent payload was still loading at first paint so it couldn't run
|
|
9248
|
+
// `isSessionValid`). In that case `existing.conversationId` points to
|
|
9249
|
+
// a now-stale conversation whose messages bucket would otherwise be
|
|
9250
|
+
// orphaned forever — `clearSession` runs in `resetConversation` (which
|
|
9251
|
+
// we never hit here) and the rotate-id branch above only fires when
|
|
9252
|
+
// the IDs match. Mirror the frontend `useSessionManager` hydrate
|
|
9253
|
+
// effect: clean up the stale bucket before we save the new session.
|
|
9254
|
+
if (existing !== null && existing !== void 0 && existing.conversationId && existing.conversationId !== bidiSessionId) {
|
|
9255
|
+
clearSessionMessages(organizationId, existing.conversationId);
|
|
9256
|
+
}
|
|
9242
9257
|
// We have a fresh ID (either from initializer or rotation), persist it if not already there
|
|
9243
9258
|
if (!existing || existing.conversationId !== bidiSessionId) {
|
|
9244
9259
|
saveSession(organizationId, bidiSessionId, {
|
|
@@ -9264,6 +9279,29 @@
|
|
|
9264
9279
|
}
|
|
9265
9280
|
}, [messages, organizationId]);
|
|
9266
9281
|
|
|
9282
|
+
// ── Sliding-window trim: drop the oldest TRIM_BATCH messages when state
|
|
9283
|
+
// exceeds CAP. Runs only when no message is mid-stream so we never slice
|
|
9284
|
+
// through a partial assistant token. Mutates React state (matches the
|
|
9285
|
+
// intended UX — old scrollback drops in front of the user once the cap is
|
|
9286
|
+
// hit, keeping what's on screen identical to what's in localStorage).
|
|
9287
|
+
reactExports.useEffect(() => {
|
|
9288
|
+
if (isConversationLoading || isLoading) return;
|
|
9289
|
+
if (messages.length <= resolvedMessageLimits.cap) return;
|
|
9290
|
+
setMessages(prev => applyMessageCap(prev, resolvedMessageLimits.cap, resolvedMessageLimits.trimBatch));
|
|
9291
|
+
}, [messages.length, isConversationLoading, isLoading, resolvedMessageLimits.cap, resolvedMessageLimits.trimBatch]);
|
|
9292
|
+
|
|
9293
|
+
// ── Persist a settled snapshot of the transcript. We write only when
|
|
9294
|
+
// streaming/voice traffic has quiesced so a partial assistant message
|
|
9295
|
+
// never lands in localStorage — on reload the next snapshot will reflect
|
|
9296
|
+
// the completed turn. Skipped while history is hydrating (initial mount
|
|
9297
|
+
// race) so the first restore isn't immediately overwritten by an empty
|
|
9298
|
+
// pre-hydrate state.
|
|
9299
|
+
reactExports.useEffect(() => {
|
|
9300
|
+
if (!organizationId || !bidiSessionId) return;
|
|
9301
|
+
if (isConversationLoading || isLoading) return;
|
|
9302
|
+
saveSessionMessages(organizationId, bidiSessionId, messages, resolvedMessageLimits);
|
|
9303
|
+
}, [messages, organizationId, bidiSessionId, isConversationLoading, isLoading, resolvedMessageLimits]);
|
|
9304
|
+
|
|
9267
9305
|
// Show the onboarding form on first load; bypass it if already completed (persisted in session)
|
|
9268
9306
|
reactExports.useEffect(() => {
|
|
9269
9307
|
if (!onboardingEnabled || onboardingCompleted) return;
|
|
@@ -10252,11 +10290,11 @@
|
|
|
10252
10290
|
const rawSources = (_event$sources = event.sources) !== null && _event$sources !== void 0 ? _event$sources : event.Sources;
|
|
10253
10291
|
if (Array.isArray(rawSources) && rawSources.length > 0) {
|
|
10254
10292
|
const normalizedSources = rawSources.map(s => {
|
|
10255
|
-
var
|
|
10293
|
+
var _ref3, _s$source_type, _ref4, _s$source_id, _ref5, _s$source_name;
|
|
10256
10294
|
return {
|
|
10257
|
-
source_type: (
|
|
10258
|
-
source_id: (
|
|
10259
|
-
source_name: (
|
|
10295
|
+
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',
|
|
10296
|
+
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,
|
|
10297
|
+
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
10298
|
url: s.url
|
|
10261
10299
|
};
|
|
10262
10300
|
}).filter(s => s.source_id || s.url);
|
|
@@ -10563,6 +10601,10 @@
|
|
|
10563
10601
|
setMessages([]);
|
|
10564
10602
|
setBidiMessages([]);
|
|
10565
10603
|
|
|
10604
|
+
// Drop the localStorage transcript for the conversation we're abandoning
|
|
10605
|
+
// BEFORE we rotate to a fresh sessionId, otherwise the old bucket leaks.
|
|
10606
|
+
clearSessionMessages(organizationId, bidiSessionId);
|
|
10607
|
+
|
|
10566
10608
|
// Reset onboarding
|
|
10567
10609
|
setOnboardingActive(false);
|
|
10568
10610
|
setOnboardingCompleted(false);
|
|
@@ -10601,7 +10643,7 @@
|
|
|
10601
10643
|
setOnboardingActive(true);
|
|
10602
10644
|
}
|
|
10603
10645
|
}
|
|
10604
|
-
}, [organizationId, welcomeMessage, onboardingEnabled, onboardingQuestions]);
|
|
10646
|
+
}, [organizationId, bidiSessionId, welcomeMessage, onboardingEnabled, onboardingQuestions, inactivityTimeoutValue, inactivityTimeoutUnit]);
|
|
10605
10647
|
|
|
10606
10648
|
// Keep a stable ref to resetConversation so the timer effect does not need to
|
|
10607
10649
|
// list it as a dependency (avoids spurious re-runs — and spurious updateActivity
|
|
@@ -60277,6 +60319,64 @@
|
|
|
60277
60319
|
});
|
|
60278
60320
|
};
|
|
60279
60321
|
|
|
60322
|
+
const createSession = async config => {
|
|
60323
|
+
// Initialize chat session if needed
|
|
60324
|
+
if (config.sessionUrl) {
|
|
60325
|
+
try {
|
|
60326
|
+
const response = await fetch(config.sessionUrl, {
|
|
60327
|
+
method: "POST",
|
|
60328
|
+
headers: _objectSpread2({
|
|
60329
|
+
"Content-Type": "application/json"
|
|
60330
|
+
}, config.apiKey && {
|
|
60331
|
+
Authorization: "Bearer ".concat(config.apiKey)
|
|
60332
|
+
}),
|
|
60333
|
+
body: JSON.stringify({
|
|
60334
|
+
userId: config.userId,
|
|
60335
|
+
userName: config.userName,
|
|
60336
|
+
userEmail: config.userEmail
|
|
60337
|
+
}),
|
|
60338
|
+
credentials: "include"
|
|
60339
|
+
});
|
|
60340
|
+
const data = await response.json();
|
|
60341
|
+
return data.sessionId;
|
|
60342
|
+
} catch (error) {
|
|
60343
|
+
console.error("Session creation error:", error);
|
|
60344
|
+
return null;
|
|
60345
|
+
}
|
|
60346
|
+
}
|
|
60347
|
+
return null;
|
|
60348
|
+
};
|
|
60349
|
+
|
|
60350
|
+
/**
|
|
60351
|
+
* Fetches agent configuration from the deploy-agent endpoint
|
|
60352
|
+
* @param {string} apiBaseUrl - Base URL for the API
|
|
60353
|
+
* @param {string} orgId - Organization ID
|
|
60354
|
+
* @returns {Promise<object>} Agent configuration object
|
|
60355
|
+
*/
|
|
60356
|
+
const fetchAgentConfig = async (apiBaseUrl, orgId) => {
|
|
60357
|
+
if (!apiBaseUrl || !orgId) {
|
|
60358
|
+
throw new Error("apiBaseUrl and orgId are required");
|
|
60359
|
+
}
|
|
60360
|
+
try {
|
|
60361
|
+
const url = "".concat(apiBaseUrl.replace(/\/$/, ''), "/api/v1/deploy-agent/").concat(orgId);
|
|
60362
|
+
const response = await fetch(url, {
|
|
60363
|
+
method: "GET",
|
|
60364
|
+
headers: {
|
|
60365
|
+
"Content-Type": "application/json"
|
|
60366
|
+
},
|
|
60367
|
+
credentials: "include"
|
|
60368
|
+
});
|
|
60369
|
+
if (!response.ok) {
|
|
60370
|
+
throw new Error("HTTP error! status: ".concat(response.status));
|
|
60371
|
+
}
|
|
60372
|
+
const data = await response.json();
|
|
60373
|
+
return data;
|
|
60374
|
+
} catch (error) {
|
|
60375
|
+
console.error("Failed to fetch agent configuration:", error);
|
|
60376
|
+
throw error;
|
|
60377
|
+
}
|
|
60378
|
+
};
|
|
60379
|
+
|
|
60280
60380
|
const ChatWidget = _ref => {
|
|
60281
60381
|
var _agentConfig$concierg;
|
|
60282
60382
|
let {
|
|
@@ -60528,7 +60628,13 @@
|
|
|
60528
60628
|
onboardingEnabled: widgetConfig.onboardingEnabled,
|
|
60529
60629
|
collectionPrompt: widgetConfig.collectionPrompt,
|
|
60530
60630
|
inactivityTimeoutValue: widgetConfig.inactivityTimeoutValue,
|
|
60531
|
-
inactivityTimeoutUnit: widgetConfig.inactivityTimeoutUnit
|
|
60631
|
+
inactivityTimeoutUnit: widgetConfig.inactivityTimeoutUnit,
|
|
60632
|
+
// Sliding-window cap for the localStorage transcript. Comes from
|
|
60633
|
+
// GET /deploy-agent/:orgId → agentConfig.messageLimits (single
|
|
60634
|
+
// source of truth across surfaces). When the field is missing
|
|
60635
|
+
// (older backend / fetch in flight) the hook falls back to its
|
|
60636
|
+
// module-level defaults so chat never blocks on this.
|
|
60637
|
+
messageLimits: agentConfig === null || agentConfig === void 0 ? void 0 : agentConfig.messageLimits
|
|
60532
60638
|
} : defaultConfig);
|
|
60533
60639
|
|
|
60534
60640
|
// Feedback handler — POST to /api/v1/support/feedback
|