impact-chatbot 2.3.83 → 2.3.84

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';
@@ -1098,6 +1098,66 @@ const getFormattedApplicationName = (applicationURL) => {
1098
1098
  }
1099
1099
  };
1100
1100
 
1101
+ /**
1102
+ * Resolves a link href to an in-app route (path + search + hash) when the link
1103
+ * points to a screen of this application, otherwise returns null.
1104
+ * Handles both relative links ("/inventory-smart/create-allocation?step=0") and
1105
+ * absolute links sent by the backend
1106
+ * ("https://tapestry.test.impactsmartsuite.com/inventory-smart/create-allocation?step=0").
1107
+ */
1108
+ const resolveInternalPath = (href) => {
1109
+ if (!href || typeof href !== 'string')
1110
+ return null;
1111
+ // Non navigational protocols (mailto:, tel:, javascript:, #anchor) stay as-is
1112
+ if (/^(mailto:|tel:|javascript:)/i.test(href) || href.startsWith('#'))
1113
+ return null;
1114
+ try {
1115
+ const url = new URL(href, window.location.origin);
1116
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
1117
+ return null;
1118
+ const currentApp = window.location.pathname.split('/')[1];
1119
+ const targetApp = url.pathname.split('/')[1];
1120
+ // Same origin, or a different host of the same app (e.g. backend returns the
1121
+ // deployed host while running locally) - both resolve to a client side route
1122
+ const isInternal = url.origin === window.location.origin ||
1123
+ (!!currentApp && currentApp === targetApp);
1124
+ if (!isInternal)
1125
+ return null;
1126
+ return `${url.pathname}${url.search}${url.hash}`;
1127
+ }
1128
+ catch (e) {
1129
+ return null;
1130
+ }
1131
+ };
1132
+ /**
1133
+ * Minimizes the chat window so the user can see the screen they navigated to.
1134
+ * SmartBot (index.tsx) listens for this event and sets partialClose.
1135
+ */
1136
+ const minimizeChatBot = () => {
1137
+ window.dispatchEvent(new CustomEvent("smartBotMinimize"));
1138
+ };
1139
+ /**
1140
+ * Anchor rendered inside chatbot markdown. Internal links are navigated through
1141
+ * react-router so the chatbot is not remounted by a full page load.
1142
+ */
1143
+ const MarkdownLink = ({ href, children, ...props }) => {
1144
+ const navigate = useNavigate();
1145
+ const handleClick = (event) => {
1146
+ // Let the browser handle new tab / new window / download intents
1147
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)
1148
+ return;
1149
+ const internalPath = resolveInternalPath(href);
1150
+ if (!internalPath)
1151
+ return;
1152
+ event.preventDefault();
1153
+ const currentPath = `${window.location.pathname}${window.location.search}`;
1154
+ if (currentPath !== internalPath) {
1155
+ navigate(internalPath);
1156
+ }
1157
+ minimizeChatBot();
1158
+ };
1159
+ return (jsx("a", { href: href, target: "_self", onClick: handleClick, ...props, children: children }));
1160
+ };
1101
1161
  /**
1102
1162
  * Checks whether the input string contains meaningful HTML markup
1103
1163
  * (beyond simple inline tags like <b>, <i>, <em>, <strong>, <br>, <a>, <u>
@@ -1138,9 +1198,16 @@ const preprocessMarkdown = (content) => {
1138
1198
  .replace(/(^|\n)([^\n-•]*[a-zA-Z0-9][^\n]*)\n([-•]\s+)/g, '$1$2\n\n$3')
1139
1199
  // Handle list items with - or • appearing directly after text without newline
1140
1200
  // 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 ')
1201
+ // [^\S\n] (horizontal whitespace only) keeps this from crossing line breaks,
1202
+ // which would dedent already correctly indented nested bullets
1203
+ .replace(/([.!?:)"])[^\S\n]*([-\u2022])[^\S\n]+/g, '$1\n\n$2 ')
1204
+ // Handle nested list items with proper indentation.
1205
+ // Matching only horizontal whitespace after the newline keeps blank lines and
1206
+ // top level numbered items intact (indenting those turns them into plain text).
1207
+ // Normalize to 3 spaces: enough to nest under both "- " and "1. " parents, and
1208
+ // below the 4 space threshold that would turn the line into a code block.
1209
+ // Indents of 4 or more are left untouched (deliberate deeper nesting).
1210
+ .replace(/(\n[^\S\n]{2,3})([-\u2022]|\d+\.)[^\S\n]+/g, '\n $2 ')
1144
1211
  // Ensure double line breaks after list sections
1145
1212
  .replace(/(\n\d+\.\s+.*?)(\n\n###)/g, '$1\n$2')
1146
1213
  .replace(/(\n[-•]\s+.*?)(\n\n###)/g, '$1\n$2')
@@ -1152,8 +1219,8 @@ const preprocessMarkdown = (content) => {
1152
1219
  * Custom components for markdown rendering
1153
1220
  */
1154
1221
  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 })),
1222
+ // Custom link component that navigates in-app without reloading the page
1223
+ a: MarkdownLink,
1157
1224
  // Custom code component
1158
1225
  code: ({ children, className, ...props }) => (jsx("code", { className: `markdown-code ${className || ''}`, ...props, children: children })),
1159
1226
  // Custom pre component for code blocks
@@ -1173,13 +1240,32 @@ const markdownComponents = {
1173
1240
  * Markdown renderer component with sanitization
1174
1241
  */
1175
1242
  const TextRenderer = ({ text, thinking }) => {
1243
+ const navigate = useNavigate();
1244
+ // Click delegation for anchors inside raw HTML content (dangerouslySetInnerHTML),
1245
+ // so in-app links navigate through react-router instead of reloading the page.
1246
+ const handleHtmlClick = (event) => {
1247
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)
1248
+ return;
1249
+ const anchor = event.target.closest?.('a');
1250
+ if (!anchor || anchor.target === '_blank')
1251
+ return;
1252
+ const internalPath = resolveInternalPath(anchor.getAttribute('href'));
1253
+ if (!internalPath)
1254
+ return;
1255
+ event.preventDefault();
1256
+ const currentPath = `${window.location.pathname}${window.location.search}`;
1257
+ if (currentPath !== internalPath) {
1258
+ navigate(internalPath);
1259
+ }
1260
+ minimizeChatBot();
1261
+ };
1176
1262
  // If the input contains rich HTML, render it directly with DOMPurify sanitization
1177
1263
  if (containsRichHtml(text)) {
1178
1264
  const sanitizedHtml = DOMPurify.sanitize(text, {
1179
1265
  ADD_TAGS: ['style'],
1180
1266
  ADD_ATTR: ['style', 'class', 'target', 'rel'],
1181
1267
  });
1182
- return (jsx("div", { className: `markdown-content md-content ${thinking ? "thinking" : ""}`, dangerouslySetInnerHTML: { __html: sanitizedHtml } }));
1268
+ return (jsx("div", { className: `markdown-content md-content ${thinking ? "thinking" : ""}`, onClick: handleHtmlClick, dangerouslySetInnerHTML: { __html: sanitizedHtml } }));
1183
1269
  }
1184
1270
  // Otherwise, use the existing markdown pipeline
1185
1271
  const processedContent = preprocessMarkdown(text);
@@ -15031,13 +15117,19 @@ const SmartBot = (props) => {
15031
15117
  if (newChatId !== undefined)
15032
15118
  setUniqueChatId(newChatId);
15033
15119
  };
15120
+ // Minimize the chat window when the user follows an in-app link from a bot response
15121
+ const handleMinimizeRequest = () => {
15122
+ setPartialClose(true);
15123
+ };
15034
15124
  window.addEventListener("stepFormStreamStart", handleStepFormStreamStart);
15035
15125
  window.addEventListener("stepFormStreamEnd", handleStepFormStreamEnd);
15036
15126
  window.addEventListener("stepFormInitStateUpdate", handleStepFormInitStateUpdate);
15127
+ window.addEventListener("smartBotMinimize", handleMinimizeRequest);
15037
15128
  return () => {
15038
15129
  window.removeEventListener("stepFormStreamStart", handleStepFormStreamStart);
15039
15130
  window.removeEventListener("stepFormStreamEnd", handleStepFormStreamEnd);
15040
15131
  window.removeEventListener("stepFormInitStateUpdate", handleStepFormInitStateUpdate);
15132
+ window.removeEventListener("smartBotMinimize", handleMinimizeRequest);
15041
15133
  };
15042
15134
  }, []);
15043
15135
  const fetchCustomBotConfigurations = async () => {