impact-chatbot 2.3.83 → 2.3.85

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/index.cjs.js CHANGED
@@ -20,9 +20,9 @@ var rehypeSanitize = require('rehype-sanitize');
20
20
  var remarkBreaks = require('remark-breaks');
21
21
  var remarkGfm = require('remark-gfm');
22
22
  var DOMPurify = require('dompurify');
23
+ var reactRouterDomV5Compat = require('react-router-dom-v5-compat');
23
24
  var filterAction = require('core/actions/filterAction');
24
25
  var smartBotActions = require('core/actions/smartBotActions');
25
- var reactRouterDomV5Compat = require('react-router-dom-v5-compat');
26
26
  var RefreshIcon = require('@mui/icons-material/Refresh');
27
27
  var styled = require('styled-components');
28
28
  var material = require('@mui/material');
@@ -675,8 +675,11 @@ const handleMessageLike = async (question, liked, setLoadingState, displaySnackM
675
675
  try {
676
676
  let request;
677
677
  if (currentMode === "agent") {
678
+ // Use the session stored on this specific message. The live sessionId is
679
+ // reset once a flow completes / awaits follow-up, so relying on it would
680
+ // send an empty session_id or the session of a newer chat.
678
681
  const agentPayload = {
679
- session_id: sessionId,
682
+ session_id: activeMessage?.chatSessionId || activeMessage?.sessionId || sessionId,
680
683
  liked,
681
684
  };
682
685
  request = await likeDislikeCommentForAgent(agentPayload, baseUrl);
@@ -1120,6 +1123,66 @@ const getFormattedApplicationName = (applicationURL) => {
1120
1123
  }
1121
1124
  };
1122
1125
 
1126
+ /**
1127
+ * Resolves a link href to an in-app route (path + search + hash) when the link
1128
+ * points to a screen of this application, otherwise returns null.
1129
+ * Handles both relative links ("/inventory-smart/create-allocation?step=0") and
1130
+ * absolute links sent by the backend
1131
+ * ("https://tapestry.test.impactsmartsuite.com/inventory-smart/create-allocation?step=0").
1132
+ */
1133
+ const resolveInternalPath = (href) => {
1134
+ if (!href || typeof href !== 'string')
1135
+ return null;
1136
+ // Non navigational protocols (mailto:, tel:, javascript:, #anchor) stay as-is
1137
+ if (/^(mailto:|tel:|javascript:)/i.test(href) || href.startsWith('#'))
1138
+ return null;
1139
+ try {
1140
+ const url = new URL(href, window.location.origin);
1141
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
1142
+ return null;
1143
+ const currentApp = window.location.pathname.split('/')[1];
1144
+ const targetApp = url.pathname.split('/')[1];
1145
+ // Same origin, or a different host of the same app (e.g. backend returns the
1146
+ // deployed host while running locally) - both resolve to a client side route
1147
+ const isInternal = url.origin === window.location.origin ||
1148
+ (!!currentApp && currentApp === targetApp);
1149
+ if (!isInternal)
1150
+ return null;
1151
+ return `${url.pathname}${url.search}${url.hash}`;
1152
+ }
1153
+ catch (e) {
1154
+ return null;
1155
+ }
1156
+ };
1157
+ /**
1158
+ * Minimizes the chat window so the user can see the screen they navigated to.
1159
+ * SmartBot (index.tsx) listens for this event and sets partialClose.
1160
+ */
1161
+ const minimizeChatBot = () => {
1162
+ window.dispatchEvent(new CustomEvent("smartBotMinimize"));
1163
+ };
1164
+ /**
1165
+ * Anchor rendered inside chatbot markdown. Internal links are navigated through
1166
+ * react-router so the chatbot is not remounted by a full page load.
1167
+ */
1168
+ const MarkdownLink = ({ href, children, ...props }) => {
1169
+ const navigate = reactRouterDomV5Compat.useNavigate();
1170
+ const handleClick = (event) => {
1171
+ // Let the browser handle new tab / new window / download intents
1172
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)
1173
+ return;
1174
+ const internalPath = resolveInternalPath(href);
1175
+ if (!internalPath)
1176
+ return;
1177
+ event.preventDefault();
1178
+ const currentPath = `${window.location.pathname}${window.location.search}`;
1179
+ if (currentPath !== internalPath) {
1180
+ navigate(internalPath);
1181
+ }
1182
+ minimizeChatBot();
1183
+ };
1184
+ return (jsxRuntime.jsx("a", { href: href, target: "_self", onClick: handleClick, ...props, children: children }));
1185
+ };
1123
1186
  /**
1124
1187
  * Checks whether the input string contains meaningful HTML markup
1125
1188
  * (beyond simple inline tags like <b>, <i>, <em>, <strong>, <br>, <a>, <u>
@@ -1160,9 +1223,16 @@ const preprocessMarkdown = (content) => {
1160
1223
  .replace(/(^|\n)([^\n-•]*[a-zA-Z0-9][^\n]*)\n([-•]\s+)/g, '$1$2\n\n$3')
1161
1224
  // Handle list items with - or • appearing directly after text without newline
1162
1225
  // Only treat as a new bullet when preceded by sentence-ending punctuation (to avoid splitting inline hyphens like "KSO- ORLANDO")
1163
- .replace(/([.!?:)\"])\s*([-•])\s+/g, '$1\n\n$2 ')
1164
- // Handle nested list items with proper indentation
1165
- .replace(/(\n\s{2,})([-•]|\d+\.)\s+/g, '\n $2 ')
1226
+ // [^\S\n] (horizontal whitespace only) keeps this from crossing line breaks,
1227
+ // which would dedent already correctly indented nested bullets
1228
+ .replace(/([.!?:)"])[^\S\n]*([-\u2022])[^\S\n]+/g, '$1\n\n$2 ')
1229
+ // Handle nested list items with proper indentation.
1230
+ // Matching only horizontal whitespace after the newline keeps blank lines and
1231
+ // top level numbered items intact (indenting those turns them into plain text).
1232
+ // Normalize to 3 spaces: enough to nest under both "- " and "1. " parents, and
1233
+ // below the 4 space threshold that would turn the line into a code block.
1234
+ // Indents of 4 or more are left untouched (deliberate deeper nesting).
1235
+ .replace(/(\n[^\S\n]{2,3})([-\u2022]|\d+\.)[^\S\n]+/g, '\n $2 ')
1166
1236
  // Ensure double line breaks after list sections
1167
1237
  .replace(/(\n\d+\.\s+.*?)(\n\n###)/g, '$1\n$2')
1168
1238
  .replace(/(\n[-•]\s+.*?)(\n\n###)/g, '$1\n$2')
@@ -1174,8 +1244,8 @@ const preprocessMarkdown = (content) => {
1174
1244
  * Custom components for markdown rendering
1175
1245
  */
1176
1246
  const markdownComponents = {
1177
- // Custom link component that opens in the same tab
1178
- a: ({ href, children, ...props }) => (jsxRuntime.jsx("a", { href: href, target: "_self", ...props, children: children })),
1247
+ // Custom link component that navigates in-app without reloading the page
1248
+ a: MarkdownLink,
1179
1249
  // Custom code component
1180
1250
  code: ({ children, className, ...props }) => (jsxRuntime.jsx("code", { className: `markdown-code ${className || ''}`, ...props, children: children })),
1181
1251
  // Custom pre component for code blocks
@@ -1195,13 +1265,32 @@ const markdownComponents = {
1195
1265
  * Markdown renderer component with sanitization
1196
1266
  */
1197
1267
  const TextRenderer = ({ text, thinking }) => {
1268
+ const navigate = reactRouterDomV5Compat.useNavigate();
1269
+ // Click delegation for anchors inside raw HTML content (dangerouslySetInnerHTML),
1270
+ // so in-app links navigate through react-router instead of reloading the page.
1271
+ const handleHtmlClick = (event) => {
1272
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)
1273
+ return;
1274
+ const anchor = event.target.closest?.('a');
1275
+ if (!anchor || anchor.target === '_blank')
1276
+ return;
1277
+ const internalPath = resolveInternalPath(anchor.getAttribute('href'));
1278
+ if (!internalPath)
1279
+ return;
1280
+ event.preventDefault();
1281
+ const currentPath = `${window.location.pathname}${window.location.search}`;
1282
+ if (currentPath !== internalPath) {
1283
+ navigate(internalPath);
1284
+ }
1285
+ minimizeChatBot();
1286
+ };
1198
1287
  // If the input contains rich HTML, render it directly with DOMPurify sanitization
1199
1288
  if (containsRichHtml(text)) {
1200
1289
  const sanitizedHtml = DOMPurify.sanitize(text, {
1201
1290
  ADD_TAGS: ['style'],
1202
1291
  ADD_ATTR: ['style', 'class', 'target', 'rel'],
1203
1292
  });
1204
- return (jsxRuntime.jsx("div", { className: `markdown-content md-content ${thinking ? "thinking" : ""}`, dangerouslySetInnerHTML: { __html: sanitizedHtml } }));
1293
+ return (jsxRuntime.jsx("div", { className: `markdown-content md-content ${thinking ? "thinking" : ""}`, onClick: handleHtmlClick, dangerouslySetInnerHTML: { __html: sanitizedHtml } }));
1205
1294
  }
1206
1295
  // Otherwise, use the existing markdown pipeline
1207
1296
  const processedContent = preprocessMarkdown(text);
@@ -5251,6 +5340,13 @@ const sseevent = (message, messageToStoreRef) => {
5251
5340
  ? parsedData.chat_id
5252
5341
  : "";
5253
5342
  messageToStoreRef.current.sessionId = parsedData?.session_id;
5343
+ // Sticky copy of the session id this message belongs to. sessionId gets
5344
+ // cleared on completed/follow-up so the next user message starts a fresh
5345
+ // session, but message level actions (like/dislike) still need to refer
5346
+ // to the session that actually produced this response.
5347
+ if (parsedData?.session_id) {
5348
+ messageToStoreRef.current.chatSessionId = parsedData.session_id;
5349
+ }
5254
5350
  }
5255
5351
  if (messageToStoreRef.current.currentMode === "navigation" &&
5256
5352
  parsedData?.session_id) {
@@ -9597,7 +9693,12 @@ const StreamedContent = ({ botData, botProps }) => {
9597
9693
  },
9598
9694
  };
9599
9695
  const hasStepFormWidgets = !isEmpty(stepFormDataMapRef.current);
9600
- processResponse(response, botData.inputBody, currentMode, botData.utilityObject.customChatConfig, {
9696
+ // response.session_id is intentionally blank once the flow completes, so
9697
+ // keep the retained session separately and stamp it on the stored message.
9698
+ const messageChatSessionId = messageToStoreRef.current.chatSessionId ||
9699
+ messageToStoreRef.current.sessionId ||
9700
+ "";
9701
+ Promise.resolve(processResponse(response, botData.inputBody, currentMode, botData.utilityObject.customChatConfig, {
9601
9702
  newChatData: chatDataInfoRef,
9602
9703
  isTabEnabled: true,
9603
9704
  steps: stepRef.current.map(s => ({ ...s })),
@@ -9605,7 +9706,15 @@ const StreamedContent = ({ botData, botProps }) => {
9605
9706
  questions: [...questionsRef.current],
9606
9707
  questionsStepsMap: { ...questionsStepsMapRef.current },
9607
9708
  stepFormDataMap: { ...stepFormDataMapRef.current },
9608
- }, activeConversationId);
9709
+ }, activeConversationId)).then(() => {
9710
+ if (!messageChatSessionId)
9711
+ return;
9712
+ const storedMessages = chatDataInfoRef.current?.[currentMode]?.conversations?.[activeConversationId]?.messages;
9713
+ if (storedMessages?.length) {
9714
+ storedMessages[storedMessages.length - 1].chatSessionId =
9715
+ messageChatSessionId;
9716
+ }
9717
+ });
9609
9718
  // [
9610
9719
  // {
9611
9720
  // header: "Finding relevant information",
@@ -15053,13 +15162,19 @@ const SmartBot = (props) => {
15053
15162
  if (newChatId !== undefined)
15054
15163
  setUniqueChatId(newChatId);
15055
15164
  };
15165
+ // Minimize the chat window when the user follows an in-app link from a bot response
15166
+ const handleMinimizeRequest = () => {
15167
+ setPartialClose(true);
15168
+ };
15056
15169
  window.addEventListener("stepFormStreamStart", handleStepFormStreamStart);
15057
15170
  window.addEventListener("stepFormStreamEnd", handleStepFormStreamEnd);
15058
15171
  window.addEventListener("stepFormInitStateUpdate", handleStepFormInitStateUpdate);
15172
+ window.addEventListener("smartBotMinimize", handleMinimizeRequest);
15059
15173
  return () => {
15060
15174
  window.removeEventListener("stepFormStreamStart", handleStepFormStreamStart);
15061
15175
  window.removeEventListener("stepFormStreamEnd", handleStepFormStreamEnd);
15062
15176
  window.removeEventListener("stepFormInitStateUpdate", handleStepFormInitStateUpdate);
15177
+ window.removeEventListener("smartBotMinimize", handleMinimizeRequest);
15063
15178
  };
15064
15179
  }, []);
15065
15180
  const fetchCustomBotConfigurations = async () => {