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.esm.js CHANGED
@@ -17,9 +17,9 @@ import rehypeSanitize from 'rehype-sanitize';
17
17
  import remarkBreaks from 'remark-breaks';
18
18
  import remarkGfm from 'remark-gfm';
19
19
  import DOMPurify from 'dompurify';
20
+ import { useNavigate, useLocation } from 'react-router-dom-v5-compat';
20
21
  import { setSelectedFilters, setFilterConfiguration, getFilterUserConfiguration } from 'core/actions/filterAction';
21
22
  import { setChatbotContext, setStepFormStreamData, setMinimizedStreamData, setThinkingContext, setPersistedFormValues, clearPersistedFormValues, setCurrentAgentChatId, setHierarchyKeyValue, setChatbotFilterOptions, setSavedFilterSets } from 'core/actions/smartBotActions';
22
- import { useNavigate, useLocation } from 'react-router-dom-v5-compat';
23
23
  import RefreshIcon from '@mui/icons-material/Refresh';
24
24
  import styled from 'styled-components';
25
25
  import { CircularProgress, Typography, Grid } from '@mui/material';
@@ -653,8 +653,11 @@ const handleMessageLike = async (question, liked, setLoadingState, displaySnackM
653
653
  try {
654
654
  let request;
655
655
  if (currentMode === "agent") {
656
+ // Use the session stored on this specific message. The live sessionId is
657
+ // reset once a flow completes / awaits follow-up, so relying on it would
658
+ // send an empty session_id or the session of a newer chat.
656
659
  const agentPayload = {
657
- session_id: sessionId,
660
+ session_id: activeMessage?.chatSessionId || activeMessage?.sessionId || sessionId,
658
661
  liked,
659
662
  };
660
663
  request = await likeDislikeCommentForAgent(agentPayload, baseUrl);
@@ -1098,6 +1101,66 @@ const getFormattedApplicationName = (applicationURL) => {
1098
1101
  }
1099
1102
  };
1100
1103
 
1104
+ /**
1105
+ * Resolves a link href to an in-app route (path + search + hash) when the link
1106
+ * points to a screen of this application, otherwise returns null.
1107
+ * Handles both relative links ("/inventory-smart/create-allocation?step=0") and
1108
+ * absolute links sent by the backend
1109
+ * ("https://tapestry.test.impactsmartsuite.com/inventory-smart/create-allocation?step=0").
1110
+ */
1111
+ const resolveInternalPath = (href) => {
1112
+ if (!href || typeof href !== 'string')
1113
+ return null;
1114
+ // Non navigational protocols (mailto:, tel:, javascript:, #anchor) stay as-is
1115
+ if (/^(mailto:|tel:|javascript:)/i.test(href) || href.startsWith('#'))
1116
+ return null;
1117
+ try {
1118
+ const url = new URL(href, window.location.origin);
1119
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
1120
+ return null;
1121
+ const currentApp = window.location.pathname.split('/')[1];
1122
+ const targetApp = url.pathname.split('/')[1];
1123
+ // Same origin, or a different host of the same app (e.g. backend returns the
1124
+ // deployed host while running locally) - both resolve to a client side route
1125
+ const isInternal = url.origin === window.location.origin ||
1126
+ (!!currentApp && currentApp === targetApp);
1127
+ if (!isInternal)
1128
+ return null;
1129
+ return `${url.pathname}${url.search}${url.hash}`;
1130
+ }
1131
+ catch (e) {
1132
+ return null;
1133
+ }
1134
+ };
1135
+ /**
1136
+ * Minimizes the chat window so the user can see the screen they navigated to.
1137
+ * SmartBot (index.tsx) listens for this event and sets partialClose.
1138
+ */
1139
+ const minimizeChatBot = () => {
1140
+ window.dispatchEvent(new CustomEvent("smartBotMinimize"));
1141
+ };
1142
+ /**
1143
+ * Anchor rendered inside chatbot markdown. Internal links are navigated through
1144
+ * react-router so the chatbot is not remounted by a full page load.
1145
+ */
1146
+ const MarkdownLink = ({ href, children, ...props }) => {
1147
+ const navigate = useNavigate();
1148
+ const handleClick = (event) => {
1149
+ // Let the browser handle new tab / new window / download intents
1150
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)
1151
+ return;
1152
+ const internalPath = resolveInternalPath(href);
1153
+ if (!internalPath)
1154
+ return;
1155
+ event.preventDefault();
1156
+ const currentPath = `${window.location.pathname}${window.location.search}`;
1157
+ if (currentPath !== internalPath) {
1158
+ navigate(internalPath);
1159
+ }
1160
+ minimizeChatBot();
1161
+ };
1162
+ return (jsx("a", { href: href, target: "_self", onClick: handleClick, ...props, children: children }));
1163
+ };
1101
1164
  /**
1102
1165
  * Checks whether the input string contains meaningful HTML markup
1103
1166
  * (beyond simple inline tags like <b>, <i>, <em>, <strong>, <br>, <a>, <u>
@@ -1138,9 +1201,16 @@ const preprocessMarkdown = (content) => {
1138
1201
  .replace(/(^|\n)([^\n-•]*[a-zA-Z0-9][^\n]*)\n([-•]\s+)/g, '$1$2\n\n$3')
1139
1202
  // Handle list items with - or • appearing directly after text without newline
1140
1203
  // Only treat as a new bullet when preceded by sentence-ending punctuation (to avoid splitting inline hyphens like "KSO- ORLANDO")
1141
- .replace(/([.!?:)\"])\s*([-•])\s+/g, '$1\n\n$2 ')
1142
- // Handle nested list items with proper indentation
1143
- .replace(/(\n\s{2,})([-•]|\d+\.)\s+/g, '\n $2 ')
1204
+ // [^\S\n] (horizontal whitespace only) keeps this from crossing line breaks,
1205
+ // which would dedent already correctly indented nested bullets
1206
+ .replace(/([.!?:)"])[^\S\n]*([-\u2022])[^\S\n]+/g, '$1\n\n$2 ')
1207
+ // Handle nested list items with proper indentation.
1208
+ // Matching only horizontal whitespace after the newline keeps blank lines and
1209
+ // top level numbered items intact (indenting those turns them into plain text).
1210
+ // Normalize to 3 spaces: enough to nest under both "- " and "1. " parents, and
1211
+ // below the 4 space threshold that would turn the line into a code block.
1212
+ // Indents of 4 or more are left untouched (deliberate deeper nesting).
1213
+ .replace(/(\n[^\S\n]{2,3})([-\u2022]|\d+\.)[^\S\n]+/g, '\n $2 ')
1144
1214
  // Ensure double line breaks after list sections
1145
1215
  .replace(/(\n\d+\.\s+.*?)(\n\n###)/g, '$1\n$2')
1146
1216
  .replace(/(\n[-•]\s+.*?)(\n\n###)/g, '$1\n$2')
@@ -1152,8 +1222,8 @@ const preprocessMarkdown = (content) => {
1152
1222
  * Custom components for markdown rendering
1153
1223
  */
1154
1224
  const markdownComponents = {
1155
- // Custom link component that opens in the same tab
1156
- a: ({ href, children, ...props }) => (jsx("a", { href: href, target: "_self", ...props, children: children })),
1225
+ // Custom link component that navigates in-app without reloading the page
1226
+ a: MarkdownLink,
1157
1227
  // Custom code component
1158
1228
  code: ({ children, className, ...props }) => (jsx("code", { className: `markdown-code ${className || ''}`, ...props, children: children })),
1159
1229
  // Custom pre component for code blocks
@@ -1173,13 +1243,32 @@ const markdownComponents = {
1173
1243
  * Markdown renderer component with sanitization
1174
1244
  */
1175
1245
  const TextRenderer = ({ text, thinking }) => {
1246
+ const navigate = useNavigate();
1247
+ // Click delegation for anchors inside raw HTML content (dangerouslySetInnerHTML),
1248
+ // so in-app links navigate through react-router instead of reloading the page.
1249
+ const handleHtmlClick = (event) => {
1250
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)
1251
+ return;
1252
+ const anchor = event.target.closest?.('a');
1253
+ if (!anchor || anchor.target === '_blank')
1254
+ return;
1255
+ const internalPath = resolveInternalPath(anchor.getAttribute('href'));
1256
+ if (!internalPath)
1257
+ return;
1258
+ event.preventDefault();
1259
+ const currentPath = `${window.location.pathname}${window.location.search}`;
1260
+ if (currentPath !== internalPath) {
1261
+ navigate(internalPath);
1262
+ }
1263
+ minimizeChatBot();
1264
+ };
1176
1265
  // If the input contains rich HTML, render it directly with DOMPurify sanitization
1177
1266
  if (containsRichHtml(text)) {
1178
1267
  const sanitizedHtml = DOMPurify.sanitize(text, {
1179
1268
  ADD_TAGS: ['style'],
1180
1269
  ADD_ATTR: ['style', 'class', 'target', 'rel'],
1181
1270
  });
1182
- return (jsx("div", { className: `markdown-content md-content ${thinking ? "thinking" : ""}`, dangerouslySetInnerHTML: { __html: sanitizedHtml } }));
1271
+ return (jsx("div", { className: `markdown-content md-content ${thinking ? "thinking" : ""}`, onClick: handleHtmlClick, dangerouslySetInnerHTML: { __html: sanitizedHtml } }));
1183
1272
  }
1184
1273
  // Otherwise, use the existing markdown pipeline
1185
1274
  const processedContent = preprocessMarkdown(text);
@@ -5229,6 +5318,13 @@ const sseevent = (message, messageToStoreRef) => {
5229
5318
  ? parsedData.chat_id
5230
5319
  : "";
5231
5320
  messageToStoreRef.current.sessionId = parsedData?.session_id;
5321
+ // Sticky copy of the session id this message belongs to. sessionId gets
5322
+ // cleared on completed/follow-up so the next user message starts a fresh
5323
+ // session, but message level actions (like/dislike) still need to refer
5324
+ // to the session that actually produced this response.
5325
+ if (parsedData?.session_id) {
5326
+ messageToStoreRef.current.chatSessionId = parsedData.session_id;
5327
+ }
5232
5328
  }
5233
5329
  if (messageToStoreRef.current.currentMode === "navigation" &&
5234
5330
  parsedData?.session_id) {
@@ -9575,7 +9671,12 @@ const StreamedContent = ({ botData, botProps }) => {
9575
9671
  },
9576
9672
  };
9577
9673
  const hasStepFormWidgets = !isEmpty(stepFormDataMapRef.current);
9578
- processResponse(response, botData.inputBody, currentMode, botData.utilityObject.customChatConfig, {
9674
+ // response.session_id is intentionally blank once the flow completes, so
9675
+ // keep the retained session separately and stamp it on the stored message.
9676
+ const messageChatSessionId = messageToStoreRef.current.chatSessionId ||
9677
+ messageToStoreRef.current.sessionId ||
9678
+ "";
9679
+ Promise.resolve(processResponse(response, botData.inputBody, currentMode, botData.utilityObject.customChatConfig, {
9579
9680
  newChatData: chatDataInfoRef,
9580
9681
  isTabEnabled: true,
9581
9682
  steps: stepRef.current.map(s => ({ ...s })),
@@ -9583,7 +9684,15 @@ const StreamedContent = ({ botData, botProps }) => {
9583
9684
  questions: [...questionsRef.current],
9584
9685
  questionsStepsMap: { ...questionsStepsMapRef.current },
9585
9686
  stepFormDataMap: { ...stepFormDataMapRef.current },
9586
- }, activeConversationId);
9687
+ }, activeConversationId)).then(() => {
9688
+ if (!messageChatSessionId)
9689
+ return;
9690
+ const storedMessages = chatDataInfoRef.current?.[currentMode]?.conversations?.[activeConversationId]?.messages;
9691
+ if (storedMessages?.length) {
9692
+ storedMessages[storedMessages.length - 1].chatSessionId =
9693
+ messageChatSessionId;
9694
+ }
9695
+ });
9587
9696
  // [
9588
9697
  // {
9589
9698
  // header: "Finding relevant information",
@@ -15031,13 +15140,19 @@ const SmartBot = (props) => {
15031
15140
  if (newChatId !== undefined)
15032
15141
  setUniqueChatId(newChatId);
15033
15142
  };
15143
+ // Minimize the chat window when the user follows an in-app link from a bot response
15144
+ const handleMinimizeRequest = () => {
15145
+ setPartialClose(true);
15146
+ };
15034
15147
  window.addEventListener("stepFormStreamStart", handleStepFormStreamStart);
15035
15148
  window.addEventListener("stepFormStreamEnd", handleStepFormStreamEnd);
15036
15149
  window.addEventListener("stepFormInitStateUpdate", handleStepFormInitStateUpdate);
15150
+ window.addEventListener("smartBotMinimize", handleMinimizeRequest);
15037
15151
  return () => {
15038
15152
  window.removeEventListener("stepFormStreamStart", handleStepFormStreamStart);
15039
15153
  window.removeEventListener("stepFormStreamEnd", handleStepFormStreamEnd);
15040
15154
  window.removeEventListener("stepFormInitStateUpdate", handleStepFormInitStateUpdate);
15155
+ window.removeEventListener("smartBotMinimize", handleMinimizeRequest);
15041
15156
  };
15042
15157
  }, []);
15043
15158
  const fetchCustomBotConfigurations = async () => {