@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.
@@ -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 createSession = async config => {
8668
- // Initialize chat session if needed
8669
- if (config.sessionUrl) {
8670
- try {
8671
- const response = await fetch(config.sessionUrl, {
8672
- method: "POST",
8673
- headers: _objectSpread2({
8674
- "Content-Type": "application/json"
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
- * Fetches conversation history for a given org and conversation
8697
- * @param {string} apiBaseUrl - Base URL for the API
8698
- * @param {string} orgId - Organization ID
8699
- * @param {string} conversationId - Conversation ID
8700
- * @returns {Promise<Array>} Array of messages
8701
- */
8702
- const fetchConversationHistory = async (apiBaseUrl, orgId, conversationId) => {
8703
- if (!apiBaseUrl || !orgId || !conversationId) {
8704
- throw new Error("apiBaseUrl, orgId, and conversationId are required");
8705
- }
8706
- const url = "".concat(apiBaseUrl.replace(/\/$/, ''), "/api/v1/conversations/").concat(orgId, "/").concat(conversationId);
8707
- const response = await fetch(url, {
8708
- method: "GET",
8709
- headers: {
8710
- "Content-Type": "application/json"
8711
- },
8712
- credentials: "include"
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
- if (response.status === 404) {
8715
- return []; // No history for this conversation yet
8716
- }
8717
- if (!response.ok) {
8718
- throw new Error("HTTP error! status: ".concat(response.status));
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
- * Fetches agent configuration from the deploy-agent endpoint
8726
- * @param {string} apiBaseUrl - Base URL for the API
8727
- * @param {string} orgId - Organization ID
8728
- * @returns {Promise<object>} Agent configuration object
8729
- */
8730
- const fetchAgentConfig = async (apiBaseUrl, orgId) => {
8731
- if (!apiBaseUrl || !orgId) {
8732
- throw new Error("apiBaseUrl and orgId are required");
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
- const url = "".concat(apiBaseUrl.replace(/\/$/, ''), "/api/v1/deploy-agent/").concat(orgId);
8736
- const response = await fetch(url, {
8737
- method: "GET",
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 (_unused) {
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 (_unused2) {}
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 (_unused3) {}
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 (_unused4) {}
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 (_unused5) {}
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
- } catch (_unused6) {}
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 (_unused7) {}
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 (_unused8) {
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 — fetch conversation history once
9144
- if (!historyLoadedRef.current && apiBaseUrl) {
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
- setIsConversationLoading(true);
9147
- fetchConversationHistory(apiBaseUrl, organizationId, bidiSessionId).then(msgs => {
9148
- // Filter out onboarding field messages (name/email/phone) so they don't appear as chat bubbles
9149
- const ONBOARDING_TYPES = ['userName', 'email', 'phone', 'fullname', 'mobileno', 'name'];
9150
- const chatMsgs = msgs.filter(msg => {
9151
- const msgType = msg.type || msg.messageType;
9152
- if (msgType && ONBOARDING_TYPES.includes(msgType)) return false;
9153
- return true;
9154
- });
9155
- if (chatMsgs.length > 0) {
9156
- historyHasMessagesRef.current = true;
9157
- const historyMessages = chatMsgs.map((msg, index) => {
9158
- var _msg$Sources;
9159
- // Normalize sources — backend may store them as "Sources" (capital) with
9160
- // "sourceid" (no underscore). Map to the shape MessageSources expects.
9161
- const rawSources = (_msg$Sources = msg.Sources) !== null && _msg$Sources !== void 0 ? _msg$Sources : msg.sources;
9162
- const sources = Array.isArray(rawSources) ? rawSources.map(s => {
9163
- var _ref3, _s$source_type, _s$source_id, _ref4, _s$source_name;
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
- // If the API didn't return prompts on any message, restore from localStorage
9185
- const hasAnyPrompts = historyMessages.some(m => m.role === 'assistant' && Array.isArray(m.prompts) && m.prompts.length > 0);
9186
- if (!hasAnyPrompts) {
9187
- const savedPrompts = getPromptSuggestions(organizationId);
9188
- if (savedPrompts.length > 0) {
9189
- // Attach saved prompts to the last assistant message
9190
- for (let i = historyMessages.length - 1; i >= 0; i -= 1) {
9191
- if (historyMessages[i].role === 'assistant') {
9192
- historyMessages[i] = _objectSpread2(_objectSpread2({}, historyMessages[i]), {}, {
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
- }).catch(err => {
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 _ref5, _s$source_type2, _ref6, _s$source_id2, _ref7, _s$source_name2;
10293
+ var _ref3, _s$source_type, _ref4, _s$source_id, _ref5, _s$source_name;
10256
10294
  return {
10257
- source_type: (_ref5 = (_s$source_type2 = s.source_type) !== null && _s$source_type2 !== void 0 ? _s$source_type2 : s.sourceType) !== null && _ref5 !== void 0 ? _ref5 : 'file',
10258
- source_id: (_ref6 = (_s$source_id2 = s.source_id) !== null && _s$source_id2 !== void 0 ? _s$source_id2 : s.sourceid) !== null && _ref6 !== void 0 ? _ref6 : s.sourceId,
10259
- source_name: (_ref7 = (_s$source_name2 = s.source_name) !== null && _s$source_name2 !== void 0 ? _s$source_name2 : s.sourceName) !== null && _ref7 !== void 0 ? _ref7 : '',
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