impact-chatbot 2.3.60 → 2.3.62

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
@@ -1,7 +1,7 @@
1
1
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
2
2
  import { cloneDeep, isEmpty as isEmpty$1, isNull, isArray as isArray$1, isObject, throttle } from 'lodash';
3
3
  import * as React from 'react';
4
- import { useCallback, useState, useEffect, useRef, useMemo } from 'react';
4
+ import { useCallback, useState, useEffect, useRef, createContext, useContext, useMemo } from 'react';
5
5
  import { COMBINED_CROSS_DIMENSIONAL_API, BASE_API } from 'config/api';
6
6
  import axiosInstance from 'core/Utils/axios';
7
7
  import { tenantConfigApiCache } from 'core/actions/tenantConfigActions';
@@ -18,7 +18,7 @@ import remarkBreaks from 'remark-breaks';
18
18
  import remarkGfm from 'remark-gfm';
19
19
  import DOMPurify from 'dompurify';
20
20
  import { setSelectedFilters, setFilterConfiguration, getFilterUserConfiguration } from 'core/actions/filterAction';
21
- import { setChatbotContext, setStepFormStreamData, setMinimizedStreamData, setThinkingContext, setPersistedFormValues, clearPersistedFormValues, setCurrentAgentChatId, setHierarchyKeyValue, setSavedFilterSets } from 'core/actions/smartBotActions';
21
+ import { setChatbotContext, setStepFormStreamData, setMinimizedStreamData, setThinkingContext, setPersistedFormValues, clearPersistedFormValues, setCurrentAgentChatId, setHierarchyKeyValue, setChatbotFilterOptions, setSavedFilterSets } from 'core/actions/smartBotActions';
22
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';
@@ -5203,9 +5203,28 @@ const sseevent = (message, messageToStoreRef) => {
5203
5203
  messageToStoreRef.current.navSessionId = parsedData.session_id;
5204
5204
  }
5205
5205
  if (parsedData?.is_error) {
5206
- messageToStoreRef.current.chatData.response =
5207
- messageToStoreRef.current.chatData.response +
5208
- "There is an error, please reach out to IA with this use case.";
5206
+ // Only append the error message once, even if multiple is_error chunks arrive.
5207
+ // Append as a widget item to appendedData so it renders AFTER any existing
5208
+ // widgets (tables, graphs, text widgets) rather than at the top of the response.
5209
+ if (!messageToStoreRef.current.hasErrorMessage) {
5210
+ messageToStoreRef.current.hasErrorMessage = true;
5211
+ const errorMsg = parsedData?.message && parsedData.message !== "[DONE]"
5212
+ ? parsedData.message
5213
+ : "There is an error, please reach out to IA with this use case.";
5214
+ const errorWidget = { type: "text", response: errorMsg };
5215
+ const prevAppended = messageToStoreRef.current.appendedData;
5216
+ if (Array.isArray(prevAppended) && prevAppended.length > 0) {
5217
+ messageToStoreRef.current.appendedData = [...prevAppended, errorWidget];
5218
+ }
5219
+ else if (prevAppended && typeof prevAppended === "object" && Object.keys(prevAppended).length > 0) {
5220
+ messageToStoreRef.current.appendedData = [prevAppended, errorWidget];
5221
+ }
5222
+ else {
5223
+ // No existing widgets — put on chatData.response instead
5224
+ messageToStoreRef.current.chatData.response =
5225
+ messageToStoreRef.current.chatData.response + errorMsg;
5226
+ }
5227
+ }
5209
5228
  // Still process completed/follow-up status even on error chunks
5210
5229
  // so that initValue is set correctly and dummyButton is suppressed
5211
5230
  if (parsedData?.status === "completed" ||
@@ -5690,6 +5709,28 @@ const ButtonContent = ({ bodyText, isFormDisabled = false, isStepFormSubmit = fa
5690
5709
  window.dispatchEvent(new CustomEvent("stepFormStreamEnd"));
5691
5710
  dispatch(setStepFormStreamData({ status: "error", chunks: chunksRef, sessionId }));
5692
5711
  });
5712
+ // Handle stream closure — fires when the HTTP request completes successfully.
5713
+ // If [DONE] was received, the "message" handler already dispatched status:"done"
5714
+ // and set stepFormStreamControl.isStreaming = false.
5715
+ // If [DONE] was NOT received, the connection closed abruptly mid-processing.
5716
+ sourceRef.current.addEventListener("close", () => {
5717
+ if (stepFormStreamControl.isStreaming) {
5718
+ stepFormStreamControl.isStreaming = false;
5719
+ stepFormStreamControl.abort = null;
5720
+ window.dispatchEvent(new CustomEvent("stepFormStreamEnd"));
5721
+ // Append the abrupt close message as a content chunk so TabularContent
5722
+ // renders it at the bottom (after any already-received widgets/steps)
5723
+ chunksRef.push({
5724
+ status: "content",
5725
+ message: "The requested Task has ended unexpectedly, please retry again",
5726
+ });
5727
+ dispatch(setStepFormStreamData({
5728
+ status: "error",
5729
+ chunks: [...chunksRef],
5730
+ sessionId,
5731
+ }));
5732
+ }
5733
+ });
5693
5734
  };
5694
5735
  const renderButtons = () => {
5695
5736
  if (!Array.isArray(bodyText.buttons)) {
@@ -6044,6 +6085,563 @@ var SvgReasoningIcon = function SvgReasoningIcon(props) {
6044
6085
  })))));
6045
6086
  };
6046
6087
 
6088
+ /**
6089
+ * Generic hook for cross-filter cascading functionality.
6090
+ * When a filter's value changes, downstream filters' options are re-fetched
6091
+ * with the current selections of upstream filters included in the payload.
6092
+ *
6093
+ * @param {Object} config
6094
+ * @param {Array} config.filters - Array of filter configs. Each must have at minimum:
6095
+ * { paramName, dimension?, column_name?, attribute_name?, display_type?, ... }
6096
+ * The order in this array determines the cascading hierarchy (index 0 = most upstream).
6097
+ * @param {Function} config.fetchOptionsFn - Async function to fetch options for a filter.
6098
+ * Signature: (filterConfig, existingSelections, allFilters) => Promise<Array<{label, value}>>
6099
+ * - filterConfig: the config of the filter whose options we're fetching
6100
+ * - existingSelections: array of { attributeName, filterName, values, checkAll } for upstream filters
6101
+ * - allFilters: the full filters array (for reference/lookup)
6102
+ * @param {Object} config.initialSelections - Optional. { paramName: value[] } to preload selections.
6103
+ * @param {boolean} config.fetchOnMount - Whether to fetch initial options for all filters on mount. Default: true.
6104
+ * @param {Function} config.onSelectionChange - Optional callback fired after any filter value changes.
6105
+ * Signature: (paramName, selectedValues, allSelections) => void
6106
+ *
6107
+ * @returns {Object}
6108
+ * - optionsMap: { paramName: Array<{label, value}> } — current available options per filter
6109
+ * - loadingMap: { paramName: boolean } — loading state per filter
6110
+ * - selectionsMap: { paramName: value[] } — current selections per filter
6111
+ * - onFilterChange: (paramName, selectedValues) => void — call when user changes a filter
6112
+ * - resetAll: () => void — reset all selections and re-fetch initial options
6113
+ * - resetFilter: (paramName) => void — reset a single filter and its downstream filters
6114
+ */
6115
+ const useCrossFilterCascading = ({ filters = [], fetchOptionsFn, initialSelections = {}, fetchOnMount = true, onSelectionChange, }) => {
6116
+ // State maps
6117
+ const [optionsMap, setOptionsMap] = useState({});
6118
+ const [loadingMap, setLoadingMap] = useState({});
6119
+ const [selectionsMap, setSelectionsMap] = useState(() => {
6120
+ // Initialize from initialSelections
6121
+ const initial = {};
6122
+ filters.forEach((f) => {
6123
+ const paramName = f.paramName || f.param_name || f.column_name || f.attribute_name;
6124
+ initial[paramName] = initialSelections[paramName] || [];
6125
+ });
6126
+ return initial;
6127
+ });
6128
+ // Refs for latest state access in async operations
6129
+ const selectionsRef = useRef(selectionsMap);
6130
+ const filtersRef = useRef(filters);
6131
+ const fetchFnRef = useRef(fetchOptionsFn);
6132
+ const mountedRef = useRef(true);
6133
+ // Keep refs in sync
6134
+ useEffect(() => {
6135
+ selectionsRef.current = selectionsMap;
6136
+ }, [selectionsMap]);
6137
+ useEffect(() => {
6138
+ filtersRef.current = filters;
6139
+ }, [filters]);
6140
+ useEffect(() => {
6141
+ fetchFnRef.current = fetchOptionsFn;
6142
+ }, [fetchOptionsFn]);
6143
+ useEffect(() => {
6144
+ return () => {
6145
+ mountedRef.current = false;
6146
+ };
6147
+ }, []);
6148
+ /**
6149
+ * Get the paramName from a filter config object (supports multiple naming conventions)
6150
+ */
6151
+ const getParamName = useCallback((filter) => {
6152
+ return filter.paramName || filter.param_name || filter.column_name || filter.attribute_name;
6153
+ }, []);
6154
+ /**
6155
+ * Get the index of a filter in the hierarchy by its paramName
6156
+ */
6157
+ const getFilterIndex = useCallback((paramName) => {
6158
+ return filtersRef.current.findIndex((f) => getParamName(f) === paramName);
6159
+ }, [getParamName]);
6160
+ /**
6161
+ * Get all filters that are downstream (higher index) of a given filter
6162
+ */
6163
+ const getDownstreamFilters = useCallback((paramName) => {
6164
+ const index = getFilterIndex(paramName);
6165
+ if (index === -1)
6166
+ return [];
6167
+ return filtersRef.current.slice(index + 1);
6168
+ }, [getFilterIndex]);
6169
+ /**
6170
+ * Get all filters that are upstream (lower index) of a given filter
6171
+ */
6172
+ const getUpstreamFilters = useCallback((paramName) => {
6173
+ const index = getFilterIndex(paramName);
6174
+ if (index <= 0)
6175
+ return [];
6176
+ return filtersRef.current.slice(0, index);
6177
+ }, [getFilterIndex]);
6178
+ /**
6179
+ * Build the existing selections payload for upstream filters (for cascading API call)
6180
+ */
6181
+ const buildExistingSelections = useCallback((paramName) => {
6182
+ const upstreamFilters = getUpstreamFilters(paramName);
6183
+ const currentSelections = selectionsRef.current;
6184
+ return upstreamFilters
6185
+ .filter((f) => {
6186
+ const pName = getParamName(f);
6187
+ const vals = currentSelections[pName];
6188
+ return vals && vals.length > 0;
6189
+ })
6190
+ .map((f) => {
6191
+ const pName = getParamName(f);
6192
+ return {
6193
+ filterName: f.label || f.name || pName,
6194
+ attributeName: f.column_name || f.attribute_name || pName,
6195
+ values: currentSelections[pName] || [],
6196
+ checkAll: false,
6197
+ };
6198
+ });
6199
+ }, [getUpstreamFilters, getParamName]);
6200
+ /**
6201
+ * Fetch options for a single filter
6202
+ */
6203
+ const fetchOptionsForFilter = useCallback(async (filterConfig) => {
6204
+ const paramName = getParamName(filterConfig);
6205
+ if (!fetchFnRef.current)
6206
+ return;
6207
+ // Set loading
6208
+ setLoadingMap((prev) => ({ ...prev, [paramName]: true }));
6209
+ try {
6210
+ const existingSelections = buildExistingSelections(paramName);
6211
+ const options = await fetchFnRef.current(filterConfig, existingSelections, filtersRef.current);
6212
+ if (!mountedRef.current)
6213
+ return;
6214
+ // Check if the response contains extra options for other filters
6215
+ const extraOptionsMap = options?.__extraOptionsMap;
6216
+ if (extraOptionsMap && typeof extraOptionsMap === "object") {
6217
+ // Distribute extra options to matching filters
6218
+ setOptionsMap((prev) => {
6219
+ const updated = { ...prev, [paramName]: options || [] };
6220
+ Object.keys(extraOptionsMap).forEach((key) => {
6221
+ // Only populate if this key matches a known filter in our hierarchy
6222
+ const matchingFilter = filtersRef.current.find((f) => getParamName(f) === key || f.column_name === key || f.attribute_name === key);
6223
+ if (matchingFilter) {
6224
+ const matchParamName = getParamName(matchingFilter);
6225
+ updated[matchParamName] = extraOptionsMap[key];
6226
+ }
6227
+ });
6228
+ return updated;
6229
+ });
6230
+ // Clear loading for extra filters that were populated
6231
+ setLoadingMap((prev) => {
6232
+ const updated = { ...prev, [paramName]: false };
6233
+ Object.keys(extraOptionsMap).forEach((key) => {
6234
+ const matchingFilter = filtersRef.current.find((f) => getParamName(f) === key || f.column_name === key || f.attribute_name === key);
6235
+ if (matchingFilter) {
6236
+ updated[getParamName(matchingFilter)] = false;
6237
+ }
6238
+ });
6239
+ return updated;
6240
+ });
6241
+ }
6242
+ else {
6243
+ setOptionsMap((prev) => ({ ...prev, [paramName]: options || [] }));
6244
+ setLoadingMap((prev) => ({ ...prev, [paramName]: false }));
6245
+ }
6246
+ }
6247
+ catch (error) {
6248
+ console.error(`[useCrossFilterCascading] Error fetching options for ${paramName}:`, error);
6249
+ if (mountedRef.current) {
6250
+ setOptionsMap((prev) => ({ ...prev, [paramName]: [] }));
6251
+ setLoadingMap((prev) => ({ ...prev, [paramName]: false }));
6252
+ }
6253
+ }
6254
+ }, [getParamName, buildExistingSelections]);
6255
+ /**
6256
+ * Fetch options for multiple filters (used for initial load and cascade refresh)
6257
+ */
6258
+ const fetchOptionsForFilters = useCallback(async (filterConfigs) => {
6259
+ await Promise.all(filterConfigs.map((f) => fetchOptionsForFilter(f)));
6260
+ }, [fetchOptionsForFilter]);
6261
+ /**
6262
+ * Handle a filter value change — triggers cascading for downstream filters
6263
+ */
6264
+ const onFilterChange = useCallback((paramName, selectedValues) => {
6265
+ const values = Array.isArray(selectedValues) ? selectedValues : [selectedValues];
6266
+ // Update selections
6267
+ setSelectionsMap((prev) => {
6268
+ const updated = { ...prev, [paramName]: values };
6269
+ selectionsRef.current = updated;
6270
+ // Notify consumer
6271
+ if (onSelectionChange) {
6272
+ onSelectionChange(paramName, values, updated);
6273
+ }
6274
+ return updated;
6275
+ });
6276
+ // Only clear downstream if the filter is being emptied (user cleared the filter).
6277
+ // When a value is being selected/changed while downstream filters already have
6278
+ // selections, preserve them — the dropdown close handler will decide whether
6279
+ // to refetch downstream options.
6280
+ if (values.length === 0) {
6281
+ const downstreamFilters = getDownstreamFilters(paramName);
6282
+ if (downstreamFilters.length > 0) {
6283
+ // Clear downstream selections
6284
+ setSelectionsMap((prev) => {
6285
+ const updated = { ...prev };
6286
+ downstreamFilters.forEach((f) => {
6287
+ const pName = getParamName(f);
6288
+ updated[pName] = [];
6289
+ });
6290
+ selectionsRef.current = updated;
6291
+ return updated;
6292
+ });
6293
+ // Clear downstream options
6294
+ const downstreamParamNames = downstreamFilters.map(getParamName);
6295
+ setOptionsMap((prev) => {
6296
+ const updated = { ...prev };
6297
+ downstreamParamNames.forEach((pName) => {
6298
+ updated[pName] = [];
6299
+ });
6300
+ return updated;
6301
+ });
6302
+ }
6303
+ }
6304
+ }, [getDownstreamFilters, getParamName, fetchOptionsForFilter, onSelectionChange]);
6305
+ /**
6306
+ * Reset all filters — clears selections and re-fetches initial options
6307
+ */
6308
+ const resetAll = useCallback(() => {
6309
+ const initial = {};
6310
+ filtersRef.current.forEach((f) => {
6311
+ initial[getParamName(f)] = [];
6312
+ });
6313
+ setSelectionsMap(initial);
6314
+ selectionsRef.current = initial;
6315
+ setOptionsMap({});
6316
+ if (fetchFnRef.current && filtersRef.current.length > 0) {
6317
+ // Fetch options for the first filter (others will cascade)
6318
+ fetchOptionsForFilter(filtersRef.current[0]);
6319
+ }
6320
+ }, [getParamName, fetchOptionsForFilter]);
6321
+ /**
6322
+ * Reset a specific filter and all its downstream filters
6323
+ */
6324
+ const resetFilter = useCallback((paramName) => {
6325
+ const downstreamFilters = getDownstreamFilters(paramName);
6326
+ const allToReset = [paramName, ...downstreamFilters.map(getParamName)];
6327
+ setSelectionsMap((prev) => {
6328
+ const updated = { ...prev };
6329
+ allToReset.forEach((pName) => {
6330
+ updated[pName] = [];
6331
+ });
6332
+ selectionsRef.current = updated;
6333
+ return updated;
6334
+ });
6335
+ // Re-fetch options for the reset filter itself
6336
+ const filterConfig = filtersRef.current.find((f) => getParamName(f) === paramName);
6337
+ if (filterConfig) {
6338
+ fetchOptionsForFilter(filterConfig);
6339
+ }
6340
+ }, [getDownstreamFilters, getParamName, fetchOptionsForFilter]);
6341
+ /**
6342
+ * Fetch options for a specific filter on demand (e.g., when dropdown is opened)
6343
+ */
6344
+ const fetchOptions = useCallback((paramName) => {
6345
+ const filterConfig = filtersRef.current.find((f) => getParamName(f) === paramName);
6346
+ if (filterConfig) {
6347
+ fetchOptionsForFilter(filterConfig);
6348
+ }
6349
+ }, [getParamName, fetchOptionsForFilter]);
6350
+ /**
6351
+ * Fetch options for all downstream filters of a given filter.
6352
+ * Called when the user closes a dropdown after making a selection,
6353
+ * so downstream filters are pre-populated before the user opens them.
6354
+ */
6355
+ const fetchDownstreamOptions = useCallback((paramName) => {
6356
+ const downstreamFilters = getDownstreamFilters(paramName);
6357
+ if (downstreamFilters.length > 0) {
6358
+ fetchOptionsForFilters(downstreamFilters);
6359
+ }
6360
+ }, [getDownstreamFilters, fetchOptionsForFilters]);
6361
+ /**
6362
+ * Build selections payload that includes the triggering filter and all other
6363
+ * filters that have selections (used for upstream/reverse cascading).
6364
+ */
6365
+ const buildAllSelections = useCallback((excludeParamName) => {
6366
+ const currentSelections = selectionsRef.current;
6367
+ return filtersRef.current
6368
+ .filter((f) => {
6369
+ const pName = getParamName(f);
6370
+ if (pName === excludeParamName)
6371
+ return false;
6372
+ const vals = currentSelections[pName];
6373
+ return vals && vals.length > 0;
6374
+ })
6375
+ .map((f) => {
6376
+ const pName = getParamName(f);
6377
+ return {
6378
+ filterName: f.label || f.name || pName,
6379
+ attributeName: f.column_name || f.attribute_name || pName,
6380
+ values: currentSelections[pName] || [],
6381
+ checkAll: false,
6382
+ };
6383
+ });
6384
+ }, [getParamName]);
6385
+ /**
6386
+ * Fetch options for a single upstream filter using all current selections
6387
+ * (reverse cascading — the payload includes the triggering lower filter's values).
6388
+ */
6389
+ const fetchUpstreamFilterOptions = useCallback(async (filterConfig) => {
6390
+ const paramName = getParamName(filterConfig);
6391
+ if (!fetchFnRef.current)
6392
+ return;
6393
+ setLoadingMap((prev) => ({ ...prev, [paramName]: true }));
6394
+ try {
6395
+ const existingSelections = buildAllSelections(paramName);
6396
+ const options = await fetchFnRef.current(filterConfig, existingSelections, filtersRef.current);
6397
+ if (!mountedRef.current)
6398
+ return;
6399
+ // Handle extra options from multi-key response
6400
+ const extraOptionsMap = options?.__extraOptionsMap;
6401
+ if (extraOptionsMap && typeof extraOptionsMap === "object") {
6402
+ setOptionsMap((prev) => {
6403
+ const updated = { ...prev, [paramName]: options || [] };
6404
+ Object.keys(extraOptionsMap).forEach((key) => {
6405
+ const matchingFilter = filtersRef.current.find((f) => getParamName(f) === key || f.column_name === key || f.attribute_name === key);
6406
+ if (matchingFilter) {
6407
+ updated[getParamName(matchingFilter)] = extraOptionsMap[key];
6408
+ }
6409
+ });
6410
+ return updated;
6411
+ });
6412
+ setLoadingMap((prev) => {
6413
+ const updated = { ...prev, [paramName]: false };
6414
+ Object.keys(extraOptionsMap).forEach((key) => {
6415
+ const matchingFilter = filtersRef.current.find((f) => getParamName(f) === key || f.column_name === key || f.attribute_name === key);
6416
+ if (matchingFilter) {
6417
+ updated[getParamName(matchingFilter)] = false;
6418
+ }
6419
+ });
6420
+ return updated;
6421
+ });
6422
+ }
6423
+ else {
6424
+ setOptionsMap((prev) => ({ ...prev, [paramName]: options || [] }));
6425
+ setLoadingMap((prev) => ({ ...prev, [paramName]: false }));
6426
+ }
6427
+ }
6428
+ catch (error) {
6429
+ console.error(`[useCrossFilterCascading] Error fetching upstream options for ${paramName}:`, error);
6430
+ if (mountedRef.current) {
6431
+ setOptionsMap((prev) => ({ ...prev, [paramName]: [] }));
6432
+ setLoadingMap((prev) => ({ ...prev, [paramName]: false }));
6433
+ }
6434
+ }
6435
+ }, [getParamName, buildAllSelections]);
6436
+ /**
6437
+ * Fetch options for all upstream filters of a given filter.
6438
+ * Called on dropdown close to reverse-cascade — e.g., selecting Channel
6439
+ * triggers a fetch for Brand with Channel's selection in the payload.
6440
+ */
6441
+ const fetchUpstreamOptions = useCallback((paramName) => {
6442
+ const upstreamFilters = getUpstreamFilters(paramName);
6443
+ if (upstreamFilters.length > 0) {
6444
+ Promise.all(upstreamFilters.map((f) => fetchUpstreamFilterOptions(f)));
6445
+ }
6446
+ }, [getUpstreamFilters, fetchUpstreamFilterOptions]);
6447
+ // Fetch initial options on mount
6448
+ useEffect(() => {
6449
+ if (fetchOnMount && filters.length > 0 && fetchFnRef.current) {
6450
+ // Fetch options for the first filter (it has no upstream dependencies)
6451
+ fetchOptionsForFilter(filters[0]);
6452
+ }
6453
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6454
+ }, []);
6455
+ return {
6456
+ optionsMap,
6457
+ loadingMap,
6458
+ selectionsMap,
6459
+ onFilterChange,
6460
+ resetAll,
6461
+ resetFilter,
6462
+ fetchOptions,
6463
+ fetchDownstreamOptions,
6464
+ fetchUpstreamOptions,
6465
+ getParamName,
6466
+ };
6467
+ };
6468
+
6469
+ /**
6470
+ * Context for cross-filter cascading.
6471
+ * Provides cascading state and actions to child select components.
6472
+ *
6473
+ * Shape:
6474
+ * {
6475
+ * optionsMap: { paramName: Array<{label, value}> },
6476
+ * loadingMap: { paramName: boolean },
6477
+ * selectionsMap: { paramName: value[] },
6478
+ * onFilterChange: (paramName, selectedValues) => void,
6479
+ * resetAll: () => void,
6480
+ * resetFilter: (paramName) => void,
6481
+ * fetchOptions: (paramName) => void,
6482
+ * getParamName: (filterConfig) => string,
6483
+ * isEnabled: boolean, // flag to know if cascading is active
6484
+ * }
6485
+ */
6486
+ const CrossFilterContext = createContext(null);
6487
+ /**
6488
+ * Hook to consume cross-filter cascading context.
6489
+ * Returns null if not inside a CrossFilterProvider (graceful fallback).
6490
+ */
6491
+ const useCrossFilterContext = () => {
6492
+ return useContext(CrossFilterContext);
6493
+ };
6494
+
6495
+ /**
6496
+ * Provider component that wraps a group of select components to enable
6497
+ * cross-filter cascading between them.
6498
+ *
6499
+ * @param {Object} props
6500
+ * @param {Array} props.filters - Array of filter configs (ordered by hierarchy)
6501
+ * @param {Function} props.fetchOptionsFn - Async function to fetch options for a filter
6502
+ * @param {Object} props.initialSelections - Optional preloaded selections { paramName: values[] }
6503
+ * @param {boolean} props.fetchOnMount - Whether to fetch initial options on mount (default: true)
6504
+ * @param {Function} props.onSelectionChange - Optional callback on any filter change
6505
+ * @param {React.ReactNode} props.children - Child components (selects) that will consume the context
6506
+ */
6507
+ const CrossFilterProvider = ({ filters, fetchOptionsFn, initialSelections = {}, fetchOnMount = true, onSelectionChange = null, children, }) => {
6508
+ const cascadingState = useCrossFilterCascading({
6509
+ filters,
6510
+ fetchOptionsFn,
6511
+ initialSelections,
6512
+ fetchOnMount,
6513
+ onSelectionChange,
6514
+ });
6515
+ const contextValue = useMemo(() => ({
6516
+ ...cascadingState,
6517
+ isEnabled: true,
6518
+ }), [cascadingState]);
6519
+ return (jsx(CrossFilterContext.Provider, { value: contextValue, children: children }));
6520
+ };
6521
+
6522
+ /**
6523
+ * Adapter function that bridges the generic useCrossFilterCascading hook
6524
+ * with the existing cross-filter API used by the chatbot.
6525
+ *
6526
+ * This is the `fetchOptionsFn` passed to the CrossFilterProvider.
6527
+ *
6528
+ * @param {Object} filterConfig - The filter config for which to fetch options
6529
+ * @param {Array} existingSelections - Upstream filter selections
6530
+ * Each item: { filterName, attributeName, values, checkAll }
6531
+ * @param {Array} allFilters - Full array of all filter configs
6532
+ * @returns {Promise<Array<{label, value}>>} - Formatted options
6533
+ */
6534
+ const fetchCrossFilterOptions = async (filterConfig, existingSelections = [], allFilters = []) => {
6535
+ try {
6536
+ const attributeName = filterConfig.column_name ||
6537
+ filterConfig.attribute_name ||
6538
+ filterConfig.param_name ||
6539
+ filterConfig.paramName;
6540
+ if (!attributeName)
6541
+ return [];
6542
+ const dimension = filterConfig.dimension || "product";
6543
+ // Build filters array from existing selections (upstream filters with values)
6544
+ const filtersArray = existingSelections.map((selection) => {
6545
+ // Find full config for this upstream filter
6546
+ const fullConfig = allFilters.find((f) => (f.column_name || f.attribute_name || f.param_name || f.paramName) === selection.attributeName);
6547
+ const selAttrName = fullConfig?.column_name ||
6548
+ fullConfig?.attribute_name ||
6549
+ fullConfig?.param_name ||
6550
+ fullConfig?.paramName ||
6551
+ selection.attributeName;
6552
+ return {
6553
+ filter_name: selection.filterName || selAttrName,
6554
+ filter_id: selAttrName,
6555
+ filter_type: "cascaded",
6556
+ dimension: fullConfig?.dimension || dimension,
6557
+ display_type: fullConfig?.display_type || "dropdown",
6558
+ check_configuration: selection.checkAll ? [{ checkAll: true, meta: {} }] : [],
6559
+ is_mandatory: fullConfig?.is_mandatory || fullConfig?.isRequired || false,
6560
+ extra: {},
6561
+ values: selection.checkAll ? [] : (selection.values || []),
6562
+ attribute_name: selAttrName,
6563
+ operator: "in",
6564
+ display_order: fullConfig?.display_order || fullConfig?.ordering || 0,
6565
+ };
6566
+ });
6567
+ const payload = {
6568
+ attributes: [
6569
+ {
6570
+ attribute_name: attributeName,
6571
+ dimension: dimension,
6572
+ filter_type: "cascaded",
6573
+ },
6574
+ ],
6575
+ filter_type: "cascaded",
6576
+ filters: filtersArray,
6577
+ is_urm_filter: true,
6578
+ screen_name: "Chatbot",
6579
+ application_code: 1,
6580
+ };
6581
+ console.log("[crossFilterAdapter] Fetching options for:", attributeName, "payload:", JSON.stringify(payload, null, 2));
6582
+ const response = await getFilterOptions(payload)();
6583
+ console.log("[crossFilterAdapter] Response for:", attributeName, "data:", response?.data?.data);
6584
+ if (response?.data?.status && response?.data?.data) {
6585
+ const responseData = response.data.data;
6586
+ const primaryValues = responseData[attributeName];
6587
+ // Format the primary filter's options
6588
+ let primaryOptions = Array.isArray(primaryValues)
6589
+ ? primaryValues.map((value) => {
6590
+ const stringValue = String(value);
6591
+ return {
6592
+ label: replaceSpecialCharacter(stringValue),
6593
+ value: stringValue,
6594
+ };
6595
+ })
6596
+ : [];
6597
+ // Check if the response contains options for other filters (multi-key response)
6598
+ const extraOptionsMap = {};
6599
+ Object.keys(responseData).forEach((key) => {
6600
+ if (key !== attributeName && Array.isArray(responseData[key])) {
6601
+ extraOptionsMap[key] = responseData[key].map((value) => {
6602
+ const stringValue = String(value);
6603
+ return {
6604
+ label: replaceSpecialCharacter(stringValue),
6605
+ value: stringValue,
6606
+ };
6607
+ });
6608
+ }
6609
+ });
6610
+ // If primary options are empty but response has data under other keys,
6611
+ // include ALL response keys in extraOptionsMap so the hook can distribute
6612
+ // them to matching filters by column_name/attribute_name lookup.
6613
+ if (primaryOptions.length === 0 && Object.keys(extraOptionsMap).length > 0) {
6614
+ // Also check if any response key matches the paramName directly
6615
+ // (e.g., paramName is "brand" but column_name sent was different)
6616
+ const altKey = Object.keys(responseData).find((key) => {
6617
+ return Array.isArray(responseData[key]) && (key === attributeName ||
6618
+ key.toLowerCase() === attributeName.toLowerCase());
6619
+ });
6620
+ if (altKey) {
6621
+ primaryOptions = responseData[altKey].map((value) => {
6622
+ const stringValue = String(value);
6623
+ return {
6624
+ label: replaceSpecialCharacter(stringValue),
6625
+ value: stringValue,
6626
+ };
6627
+ });
6628
+ delete extraOptionsMap[altKey];
6629
+ }
6630
+ }
6631
+ // Return enriched result with extra options if available
6632
+ if (Object.keys(extraOptionsMap).length > 0) {
6633
+ primaryOptions.__extraOptionsMap = extraOptionsMap;
6634
+ }
6635
+ return primaryOptions;
6636
+ }
6637
+ return [];
6638
+ }
6639
+ catch (error) {
6640
+ console.error("[crossFilterAdapter] Error fetching options:", error);
6641
+ return [];
6642
+ }
6643
+ };
6644
+
6047
6645
  const SliderContent = ({ bodyText, isFormDisabled = false, messageIndex }) => {
6048
6646
  const formKey = `${messageIndex}_${bodyText?.paramName}`;
6049
6647
  const { header, headerOrentiation, inputPosition, label, max, min, required, disabled, } = bodyText;
@@ -6093,6 +6691,10 @@ const SelectContent = ({ bodyText, isFormDisabled = false, messageIndex }) => {
6093
6691
  const chatbotContext = useSelector((state) => state.smartBotReducer.chatbotContext);
6094
6692
  const heirarchyKeyValuePairs = useSelector((state) => state.smartBotReducer.heirarchyKeyValuePairs);
6095
6693
  const dispatch = useDispatch();
6694
+ // Cross-filter cascading context (optional — graceful fallback when not wrapped)
6695
+ const crossFilterCtx = useCrossFilterContext();
6696
+ const isCascading = crossFilterCtx?.isEnabled && paramName;
6697
+ const isMountedRef = useRef(false);
6096
6698
  if (isEmpty$1(bodyText))
6097
6699
  return null;
6098
6700
  const onChange = (selectedOptions) => {
@@ -6116,6 +6718,10 @@ const SelectContent = ({ bodyText, isFormDisabled = false, messageIndex }) => {
6116
6718
  // };
6117
6719
  dispatch(setChatbotContext(chatbotContext));
6118
6720
  dispatch(setPersistedFormValues({ [formKey]: Array.isArray(selectedOptions) ? selectedOptions : [selectedOptions] }));
6721
+ // Notify cross-filter context if cascading is active
6722
+ if (isCascading) {
6723
+ crossFilterCtx.onFilterChange(paramName, Array.isArray(value) ? value : [value]);
6724
+ }
6119
6725
  }
6120
6726
  catch (error) {
6121
6727
  console.error("Error in select handleChange", error);
@@ -6127,16 +6733,94 @@ const SelectContent = ({ bodyText, isFormDisabled = false, messageIndex }) => {
6127
6733
  setCurrentSelectedOptions([]);
6128
6734
  }
6129
6735
  }, [persistedFormValues, formKey]);
6736
+ // When cross-filter context provides new options for this filter, update local state
6130
6737
  useEffect(() => {
6131
- allOptionsRef.current = options;
6132
- const initialSlice = formatSlice(options, 0, INITIAL_DISPLAY_COUNT);
6133
- setInitialOptions(initialSlice);
6134
- setCurrentOptions(initialSlice);
6135
- }, []);
6738
+ if (isCascading && crossFilterCtx.optionsMap[paramName]) {
6739
+ const cascadedOptions = crossFilterCtx.optionsMap[paramName];
6740
+ allOptionsRef.current = cascadedOptions;
6741
+ const initialSlice = formatSlice(cascadedOptions, 0, INITIAL_DISPLAY_COUNT);
6742
+ setInitialOptions(initialSlice);
6743
+ setCurrentOptions(initialSlice);
6744
+ }
6745
+ }, [isCascading, crossFilterCtx?.optionsMap?.[paramName]]);
6746
+ // When cascading resets downstream selections, clear local selection
6747
+ // Skip on initial mount to avoid clearing persisted values when context reinitializes
6748
+ useEffect(() => {
6749
+ if (!isMountedRef.current) {
6750
+ isMountedRef.current = true;
6751
+ return;
6752
+ }
6753
+ if (isCascading) {
6754
+ const cascadedSelection = crossFilterCtx.selectionsMap[paramName];
6755
+ if (cascadedSelection && cascadedSelection.length === 0 && currentSelectedOptions.length > 0) {
6756
+ setCurrentSelectedOptions([]);
6757
+ setIsAllSelected(false);
6758
+ // Also clear from chatbotContext and persistedFormValues
6759
+ chatbotContext[bodyText?.paramName] = {
6760
+ ...chatbotContext?.[bodyText?.paramName],
6761
+ [bodyText?.paramName]: [],
6762
+ updated: true,
6763
+ };
6764
+ dispatch(setChatbotContext(chatbotContext));
6765
+ dispatch(setPersistedFormValues({ [formKey]: [] }));
6766
+ }
6767
+ }
6768
+ }, [isCascading, crossFilterCtx?.selectionsMap?.[paramName]]);
6769
+ useEffect(() => {
6770
+ // Only use static options from bodyText if NOT in cascading mode
6771
+ if (!isCascading) {
6772
+ allOptionsRef.current = options;
6773
+ const initialSlice = formatSlice(options, 0, INITIAL_DISPLAY_COUNT);
6774
+ setInitialOptions(initialSlice);
6775
+ setCurrentOptions(initialSlice);
6776
+ }
6777
+ }, [isCascading]);
6136
6778
  return (jsx("div", { style: { width: "100%", marginTop: "10px" }, children: jsx(Select, { currentOptions: currentOptions, setCurrentOptions: setCurrentOptions, label: heirarchyKeyValuePairs[paramName] || label, labelOrientation: labelOrientation,
6137
6779
  // inputPosition={inputPosition}
6138
6780
  // header={header}
6139
- isRequired: isRequired, isDisabled: isDisabled || isFormDisabled, handleChange: (selected) => onChange(selected), isCloseWhenClickOutside: true, setIsOpen: setIsOpen, isOpen: isOpen, selectedOptions: currentSelectedOptions, setSelectedOptions: setCurrentSelectedOptions, initialOptions: initialOptions, isMulti: isMulti, isSelectAll: isAllSelected, setIsSelectAll: setIsAllSelected, toggleSelectAll: true, isWithSearch: isMulti ? true : false, onMenuScrollToBottom: () => {
6781
+ isRequired: isRequired, isDisabled: isDisabled || isFormDisabled, isClearable: !(isDisabled || isFormDisabled), onClearAll: () => {
6782
+ setCurrentSelectedOptions([]);
6783
+ setIsAllSelected(false);
6784
+ chatbotContext[bodyText?.paramName] = {
6785
+ ...chatbotContext?.[bodyText?.paramName],
6786
+ [bodyText?.paramName]: [],
6787
+ updated: true,
6788
+ };
6789
+ dispatch(setChatbotContext(chatbotContext));
6790
+ dispatch(setPersistedFormValues({ [formKey]: [] }));
6791
+ // Notify cross-filter context to clear downstream filters
6792
+ if (isCascading) {
6793
+ crossFilterCtx.onFilterChange(paramName, []);
6794
+ }
6795
+ }, handleChange: (selected) => onChange(selected), isCloseWhenClickOutside: true, setIsOpen: (open) => {
6796
+ setIsOpen(open);
6797
+ if (isCascading) {
6798
+ if (open) {
6799
+ // Lazy fetch: trigger cross-filter API call when dropdown opens for the first time
6800
+ if ((!crossFilterCtx.optionsMap[paramName] || crossFilterCtx.optionsMap[paramName].length === 0) && !crossFilterCtx.loadingMap[paramName]) {
6801
+ crossFilterCtx.fetchOptions(paramName);
6802
+ }
6803
+ }
6804
+ else {
6805
+ // On dropdown close: if there's a selection, pre-fetch related filter options
6806
+ const currentSelection = crossFilterCtx.selectionsMap[paramName];
6807
+ if (currentSelection && currentSelection.length > 0) {
6808
+ // Only fetch downstream if no downstream filter already has selections
6809
+ // (avoids clearing user's existing lower-hierarchy choices)
6810
+ const hasDownstreamSelections = Object.keys(crossFilterCtx.selectionsMap).some((key) => {
6811
+ if (key === paramName)
6812
+ return false;
6813
+ const vals = crossFilterCtx.selectionsMap[key];
6814
+ return vals && vals.length > 0;
6815
+ });
6816
+ if (!hasDownstreamSelections) {
6817
+ crossFilterCtx.fetchDownstreamOptions(paramName);
6818
+ }
6819
+ crossFilterCtx.fetchUpstreamOptions(paramName);
6820
+ }
6821
+ }
6822
+ }
6823
+ }, isOpen: isOpen, selectedOptions: currentSelectedOptions, setSelectedOptions: setCurrentSelectedOptions, initialOptions: initialOptions, isMulti: isMulti, isSelectAll: isAllSelected, setIsSelectAll: setIsAllSelected, toggleSelectAll: true, isLoading: isCascading ? crossFilterCtx.loadingMap[paramName] : false, emptyMessage: isCascading && crossFilterCtx.loadingMap[paramName] ? "Loading values..." : "No options available", isWithSearch: isMulti ? true : false, onMenuScrollToBottom: () => {
6140
6824
  const allRaw = allOptionsRef.current;
6141
6825
  if (allRaw.length > 0 && currentOptions.length < allRaw.length) {
6142
6826
  const nextCount = Math.min(currentOptions.length + LOAD_MORE_COUNT, allRaw.length);
@@ -6160,10 +6844,18 @@ const SelectContent = ({ bodyText, isFormDisabled = false, messageIndex }) => {
6160
6844
  };
6161
6845
  dispatch(setChatbotContext(chatbotContext));
6162
6846
  dispatch(setPersistedFormValues({ [formKey]: currentOptions }));
6847
+ // Notify cross-filter context if cascading is active
6848
+ if (isCascading) {
6849
+ crossFilterCtx.onFilterChange(paramName, allValues);
6850
+ }
6163
6851
  }
6164
6852
  else {
6165
6853
  setCurrentSelectedOptions([]);
6166
6854
  setIsAllSelected(false);
6855
+ // Notify cross-filter context of deselection
6856
+ if (isCascading) {
6857
+ crossFilterCtx.onFilterChange(paramName, []);
6858
+ }
6167
6859
  }
6168
6860
  }, customPlaceholderAfterSelect: isAllSelected && allOptionsRef.current.length > 0
6169
6861
  ? allOptionsRef.current.length
@@ -6377,6 +7069,8 @@ const StepFormContent = ({ formData, messageIndex = 0, isFormDisabled = false, s
6377
7069
  const persistedFormValues = useSelector((state) => state.smartBotReducer.persistedFormValues);
6378
7070
  const chatbotContext = useSelector((state) => state.smartBotReducer.chatbotContext);
6379
7071
  const stepFormStreamData = useSelector((state) => state.smartBotReducer.stepFormStreamData);
7072
+ const chatbotFilterOptions = useSelector((state) => state.smartBotReducer.chatbotFilterOptions);
7073
+ const dimensionHierarchies = useSelector((state) => state.userRoleManagementService?.dimensionHierarchies);
6380
7074
  const [isFormSubmitted, setIsFormSubmitted] = useState(false);
6381
7075
  useEffect(() => {
6382
7076
  if (stepFormStreamData?.status === "streaming_start") {
@@ -6483,6 +7177,77 @@ const StepFormContent = ({ formData, messageIndex = 0, isFormDisabled = false, s
6483
7177
  return false;
6484
7178
  });
6485
7179
  }, [formData, chatbotContext, persistedFormValues, messageIndex]);
7180
+ // Extract select-type filter configs from formData for cross-filter cascading
7181
+ // and sort them based on the filters_hierarchy_order from tenant config
7182
+ const crossFilterConfigs = useMemo(() => {
7183
+ if (!formData || !Array.isArray(formData))
7184
+ return [];
7185
+ const configs = formData
7186
+ .filter((item) => item?.type === "select" && item?.data?.param_name)
7187
+ .map((item) => {
7188
+ const data = item.data;
7189
+ const paramName = data.param_name;
7190
+ // Try to find full filter config from chatbotFilterOptions (Redux)
7191
+ const fullConfig = (chatbotFilterOptions || []).find((f) => f.column_name === paramName || f.attribute_name === paramName || f.name === paramName);
7192
+ return {
7193
+ paramName: paramName,
7194
+ param_name: paramName,
7195
+ column_name: fullConfig?.column_name || paramName,
7196
+ attribute_name: fullConfig?.attribute_name || paramName,
7197
+ dimension: fullConfig?.dimension || "product",
7198
+ display_type: fullConfig?.display_type || "dropdown",
7199
+ display_order: fullConfig?.display_order || item.ordering || 0,
7200
+ label: data.label,
7201
+ name: fullConfig?.name || data.label,
7202
+ is_mandatory: data.isRequired || false,
7203
+ };
7204
+ });
7205
+ // Sort configs based on dimensionHierarchies order.
7206
+ // Filters are sorted by their position in their dimension's hierarchy list.
7207
+ // This determines the cascading order (earlier = upstream/higher hierarchy).
7208
+ if (dimensionHierarchies && Object.keys(dimensionHierarchies).length > 0) {
7209
+ configs.sort((a, b) => {
7210
+ const aHierarchy = dimensionHierarchies[a.dimension] || [];
7211
+ const bHierarchy = dimensionHierarchies[b.dimension] || [];
7212
+ const aIndex = aHierarchy.indexOf(a.column_name) !== -1
7213
+ ? aHierarchy.indexOf(a.column_name)
7214
+ : aHierarchy.indexOf(a.attribute_name) !== -1
7215
+ ? aHierarchy.indexOf(a.attribute_name)
7216
+ : aHierarchy.indexOf(a.paramName) !== -1
7217
+ ? aHierarchy.indexOf(a.paramName)
7218
+ : 999;
7219
+ const bIndex = bHierarchy.indexOf(b.column_name) !== -1
7220
+ ? bHierarchy.indexOf(b.column_name)
7221
+ : bHierarchy.indexOf(b.attribute_name) !== -1
7222
+ ? bHierarchy.indexOf(b.attribute_name)
7223
+ : bHierarchy.indexOf(b.paramName) !== -1
7224
+ ? bHierarchy.indexOf(b.paramName)
7225
+ : 999;
7226
+ return aIndex - bIndex;
7227
+ });
7228
+ }
7229
+ return configs;
7230
+ }, [formData, chatbotFilterOptions, dimensionHierarchies]);
7231
+ // Build initial selections from chatbotContext and persistedFormValues (for restore on remount)
7232
+ const crossFilterInitialSelections = useMemo(() => {
7233
+ const selections = {};
7234
+ crossFilterConfigs.forEach((cfg) => {
7235
+ const ctx = chatbotContext?.[cfg.paramName];
7236
+ if (ctx && ctx.updated && ctx[cfg.paramName]) {
7237
+ const val = ctx[cfg.paramName];
7238
+ selections[cfg.paramName] = Array.isArray(val) ? val : [val];
7239
+ }
7240
+ else {
7241
+ // Fallback: check persistedFormValues for option objects
7242
+ const persistedKey = `${messageIndex}_${cfg.paramName}`;
7243
+ const persisted = persistedFormValues?.[persistedKey];
7244
+ if (persisted && Array.isArray(persisted) && persisted.length > 0) {
7245
+ selections[cfg.paramName] = persisted.map((opt) => opt.value || opt);
7246
+ }
7247
+ }
7248
+ });
7249
+ return selections;
7250
+ }, [crossFilterConfigs, chatbotContext, persistedFormValues, messageIndex]);
6486
7251
  if (!formData || !Array.isArray(formData) || formData.length === 0) {
6487
7252
  return null;
6488
7253
  }
@@ -6544,7 +7309,7 @@ const StepFormContent = ({ formData, messageIndex = 0, isFormDisabled = false, s
6544
7309
  return null;
6545
7310
  return (jsxs("div", { className: "step-form-content", children: [showSavedFilters && filterSetOptions.length > 0 && (jsx("div", { style: { width: "100%", marginTop: "10px" }, children: jsx(Select, { currentOptions: filterSetCurrentOptions, setCurrentOptions: setFilterSetCurrentOptions, label: "Saved Filter Sets", labelOrientation: "top", isRequired: false, isDisabled: savedFilterDisabled, handleChange: (selected) => onFilterSetChange(selected), isCloseWhenClickOutside: true, setIsOpen: setIsFilterSetOpen, isOpen: isFilterSetOpen, selectedOptions: selectedFilterSet, setSelectedOptions: setSelectedFilterSet, initialOptions: filterSetOptions, isMulti: false, isClearable: true }) })), showSavedFilters && filterSetOptions.length > 0 && (jsx("hr", { style: { border: "none", borderTop: "1px solid #E0E0E0", margin: "12px 0" } })), jsx("div", { style: {
6546
7311
  ...(isFilterSelected && !isFormDisabled ? { pointerEvents: "none", opacity: 0.5 } : {}),
6547
- }, children: formFields }), buttonItems] }));
7312
+ }, children: crossFilterConfigs.length > 1 ? (jsx(CrossFilterProvider, { filters: crossFilterConfigs, fetchOptionsFn: fetchCrossFilterOptions, initialSelections: crossFilterInitialSelections, fetchOnMount: false, children: formFields })) : (formFields) }), buttonItems] }));
6548
7313
  };
6549
7314
 
6550
7315
  const useStyles$4 = makeStyles$1((theme) => ({
@@ -7429,6 +8194,26 @@ const StreamedContent = ({ botData }) => {
7429
8194
  }));
7430
8195
  return;
7431
8196
  }
8197
+ // Skip is_error chunks — already handled in AxiosEventSource sseevent()
8198
+ if (data?.is_error) {
8199
+ // Trigger completion if status is "completed"
8200
+ if (data?.status === "completed") {
8201
+ setStepsDone(true);
8202
+ setIsStreamingDone(true);
8203
+ const doneState = streamStateMap.get(streamKey);
8204
+ if (doneState)
8205
+ doneState.completed = true;
8206
+ dispatch(setMinimizedStreamData({
8207
+ isStreaming: false,
8208
+ stepHeader: questionsRef.current[questionsRef.current.length - 1] || "Completed",
8209
+ stepSubHeader: "Done",
8210
+ stepStatus: "completed",
8211
+ streamStartTime: streamStartTimeRef.current,
8212
+ actionCount: questionsRef.current.length || 1,
8213
+ }));
8214
+ }
8215
+ return;
8216
+ }
7432
8217
  if (data?.message || data?.status === "step" || data?.status === "step_form" || data?.status === "thinking" || data?.status === "questions" || data?.status === "widget") {
7433
8218
  if (data.status === "questions") {
7434
8219
  const incomingQuestions = data.widget_data?.[0]?.questions || [];
@@ -7725,9 +8510,26 @@ const StreamedContent = ({ botData }) => {
7725
8510
  stepRef.current = [...currentSteps];
7726
8511
  setSteps([...currentSteps]);
7727
8512
  }
7728
- // Set the response message for the agent_response tab
8513
+ // Show the abrupt close message at the bottom of any existing content.
8514
+ // If widgets already exist in appendedData, append the error as the last widget
8515
+ // so it renders AFTER the already-received content (not at the top).
7729
8516
  const abruptCloseMessage = "The requested Task has ended unexpectedly, please retry again";
7730
- messageToStoreRef.current.chatData.response = abruptCloseMessage;
8517
+ const hasExistingWidgetsOnErr = messageToStoreRef.current.appendedData &&
8518
+ (isArray(messageToStoreRef.current.appendedData)
8519
+ ? messageToStoreRef.current.appendedData.length > 0
8520
+ : Object.keys(messageToStoreRef.current.appendedData).length > 0);
8521
+ if (hasExistingWidgetsOnErr) {
8522
+ const errorWidget = { type: "text", response: abruptCloseMessage };
8523
+ if (isArray(messageToStoreRef.current.appendedData)) {
8524
+ messageToStoreRef.current.appendedData.push(errorWidget);
8525
+ }
8526
+ else {
8527
+ messageToStoreRef.current.appendedData = [messageToStoreRef.current.appendedData, errorWidget];
8528
+ }
8529
+ }
8530
+ else {
8531
+ messageToStoreRef.current.chatData.response = abruptCloseMessage;
8532
+ }
7731
8533
  setContent(abruptCloseMessage);
7732
8534
  // Dispatch minimized widget data
7733
8535
  dispatch(setMinimizedStreamData({
@@ -7765,9 +8567,30 @@ const StreamedContent = ({ botData }) => {
7765
8567
  stepRef.current = [...currentSteps];
7766
8568
  setSteps([...currentSteps]);
7767
8569
  }
7768
- // Set the response message for the agent_response tab
8570
+ // Show the abrupt close message. The final response array is structured as:
8571
+ // [textResponseTobeParsed, ...appendedData] — so chatData.response renders FIRST,
8572
+ // then widget items from appendedData render below it.
8573
+ // To ensure the error message appears AFTER already-rendered widgets, we append it
8574
+ // as a text widget item to appendedData instead of setting it on chatData.response.
7769
8575
  const abruptCloseMessage = "The requested Task has ended unexpectedly, please retry again";
7770
- messageToStoreRef.current.chatData.response = abruptCloseMessage;
8576
+ const hasExistingWidgets = messageToStoreRef.current.appendedData &&
8577
+ (isArray(messageToStoreRef.current.appendedData)
8578
+ ? messageToStoreRef.current.appendedData.length > 0
8579
+ : Object.keys(messageToStoreRef.current.appendedData).length > 0);
8580
+ if (hasExistingWidgets) {
8581
+ // Widgets already exist — append error as the last widget so it renders at the bottom
8582
+ const errorWidget = { type: "text", response: abruptCloseMessage };
8583
+ if (isArray(messageToStoreRef.current.appendedData)) {
8584
+ messageToStoreRef.current.appendedData.push(errorWidget);
8585
+ }
8586
+ else {
8587
+ messageToStoreRef.current.appendedData = [messageToStoreRef.current.appendedData, errorWidget];
8588
+ }
8589
+ }
8590
+ else {
8591
+ // No widgets — set on chatData.response (will be the only rendered text)
8592
+ messageToStoreRef.current.chatData.response = abruptCloseMessage;
8593
+ }
7771
8594
  setContent(abruptCloseMessage);
7772
8595
  // Stop thinking if in progress
7773
8596
  if (isThinking) {
@@ -13309,6 +14132,7 @@ const SmartBot = (props) => {
13309
14132
  // Dispatch hierarchy key-value pairs to store
13310
14133
  dispatch(setHierarchyKeyValue(hierarchyKeyValuePairs));
13311
14134
  setFilterOptions(combinedOptions);
14135
+ dispatch(setChatbotFilterOptions(combinedOptions));
13312
14136
  // Fetch saved filter sets for the plus-button menu
13313
14137
  fetchSavedFilterSets();
13314
14138
  }