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/components/message-template/components/message-content/utils/crossFilterAdapter.d.ts +16 -0
- package/dist/hooks/crossFilterCascading/CrossFilterContext.d.ts +24 -0
- package/dist/hooks/crossFilterCascading/CrossFilterProvider.d.ts +21 -0
- package/dist/hooks/crossFilterCascading/index.d.ts +3 -0
- package/dist/hooks/crossFilterCascading/useCrossFilterCascading.d.ts +46 -0
- package/dist/index.cjs.js +838 -14
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +840 -16
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -5225,9 +5225,28 @@ const sseevent = (message, messageToStoreRef) => {
|
|
|
5225
5225
|
messageToStoreRef.current.navSessionId = parsedData.session_id;
|
|
5226
5226
|
}
|
|
5227
5227
|
if (parsedData?.is_error) {
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5228
|
+
// Only append the error message once, even if multiple is_error chunks arrive.
|
|
5229
|
+
// Append as a widget item to appendedData so it renders AFTER any existing
|
|
5230
|
+
// widgets (tables, graphs, text widgets) rather than at the top of the response.
|
|
5231
|
+
if (!messageToStoreRef.current.hasErrorMessage) {
|
|
5232
|
+
messageToStoreRef.current.hasErrorMessage = true;
|
|
5233
|
+
const errorMsg = parsedData?.message && parsedData.message !== "[DONE]"
|
|
5234
|
+
? parsedData.message
|
|
5235
|
+
: "There is an error, please reach out to IA with this use case.";
|
|
5236
|
+
const errorWidget = { type: "text", response: errorMsg };
|
|
5237
|
+
const prevAppended = messageToStoreRef.current.appendedData;
|
|
5238
|
+
if (Array.isArray(prevAppended) && prevAppended.length > 0) {
|
|
5239
|
+
messageToStoreRef.current.appendedData = [...prevAppended, errorWidget];
|
|
5240
|
+
}
|
|
5241
|
+
else if (prevAppended && typeof prevAppended === "object" && Object.keys(prevAppended).length > 0) {
|
|
5242
|
+
messageToStoreRef.current.appendedData = [prevAppended, errorWidget];
|
|
5243
|
+
}
|
|
5244
|
+
else {
|
|
5245
|
+
// No existing widgets — put on chatData.response instead
|
|
5246
|
+
messageToStoreRef.current.chatData.response =
|
|
5247
|
+
messageToStoreRef.current.chatData.response + errorMsg;
|
|
5248
|
+
}
|
|
5249
|
+
}
|
|
5231
5250
|
// Still process completed/follow-up status even on error chunks
|
|
5232
5251
|
// so that initValue is set correctly and dummyButton is suppressed
|
|
5233
5252
|
if (parsedData?.status === "completed" ||
|
|
@@ -5712,6 +5731,28 @@ const ButtonContent = ({ bodyText, isFormDisabled = false, isStepFormSubmit = fa
|
|
|
5712
5731
|
window.dispatchEvent(new CustomEvent("stepFormStreamEnd"));
|
|
5713
5732
|
dispatch(smartBotActions.setStepFormStreamData({ status: "error", chunks: chunksRef, sessionId }));
|
|
5714
5733
|
});
|
|
5734
|
+
// Handle stream closure — fires when the HTTP request completes successfully.
|
|
5735
|
+
// If [DONE] was received, the "message" handler already dispatched status:"done"
|
|
5736
|
+
// and set stepFormStreamControl.isStreaming = false.
|
|
5737
|
+
// If [DONE] was NOT received, the connection closed abruptly mid-processing.
|
|
5738
|
+
sourceRef.current.addEventListener("close", () => {
|
|
5739
|
+
if (stepFormStreamControl.isStreaming) {
|
|
5740
|
+
stepFormStreamControl.isStreaming = false;
|
|
5741
|
+
stepFormStreamControl.abort = null;
|
|
5742
|
+
window.dispatchEvent(new CustomEvent("stepFormStreamEnd"));
|
|
5743
|
+
// Append the abrupt close message as a content chunk so TabularContent
|
|
5744
|
+
// renders it at the bottom (after any already-received widgets/steps)
|
|
5745
|
+
chunksRef.push({
|
|
5746
|
+
status: "content",
|
|
5747
|
+
message: "The requested Task has ended unexpectedly, please retry again",
|
|
5748
|
+
});
|
|
5749
|
+
dispatch(smartBotActions.setStepFormStreamData({
|
|
5750
|
+
status: "error",
|
|
5751
|
+
chunks: [...chunksRef],
|
|
5752
|
+
sessionId,
|
|
5753
|
+
}));
|
|
5754
|
+
}
|
|
5755
|
+
});
|
|
5715
5756
|
};
|
|
5716
5757
|
const renderButtons = () => {
|
|
5717
5758
|
if (!Array.isArray(bodyText.buttons)) {
|
|
@@ -6066,6 +6107,563 @@ var SvgReasoningIcon = function SvgReasoningIcon(props) {
|
|
|
6066
6107
|
})))));
|
|
6067
6108
|
};
|
|
6068
6109
|
|
|
6110
|
+
/**
|
|
6111
|
+
* Generic hook for cross-filter cascading functionality.
|
|
6112
|
+
* When a filter's value changes, downstream filters' options are re-fetched
|
|
6113
|
+
* with the current selections of upstream filters included in the payload.
|
|
6114
|
+
*
|
|
6115
|
+
* @param {Object} config
|
|
6116
|
+
* @param {Array} config.filters - Array of filter configs. Each must have at minimum:
|
|
6117
|
+
* { paramName, dimension?, column_name?, attribute_name?, display_type?, ... }
|
|
6118
|
+
* The order in this array determines the cascading hierarchy (index 0 = most upstream).
|
|
6119
|
+
* @param {Function} config.fetchOptionsFn - Async function to fetch options for a filter.
|
|
6120
|
+
* Signature: (filterConfig, existingSelections, allFilters) => Promise<Array<{label, value}>>
|
|
6121
|
+
* - filterConfig: the config of the filter whose options we're fetching
|
|
6122
|
+
* - existingSelections: array of { attributeName, filterName, values, checkAll } for upstream filters
|
|
6123
|
+
* - allFilters: the full filters array (for reference/lookup)
|
|
6124
|
+
* @param {Object} config.initialSelections - Optional. { paramName: value[] } to preload selections.
|
|
6125
|
+
* @param {boolean} config.fetchOnMount - Whether to fetch initial options for all filters on mount. Default: true.
|
|
6126
|
+
* @param {Function} config.onSelectionChange - Optional callback fired after any filter value changes.
|
|
6127
|
+
* Signature: (paramName, selectedValues, allSelections) => void
|
|
6128
|
+
*
|
|
6129
|
+
* @returns {Object}
|
|
6130
|
+
* - optionsMap: { paramName: Array<{label, value}> } — current available options per filter
|
|
6131
|
+
* - loadingMap: { paramName: boolean } — loading state per filter
|
|
6132
|
+
* - selectionsMap: { paramName: value[] } — current selections per filter
|
|
6133
|
+
* - onFilterChange: (paramName, selectedValues) => void — call when user changes a filter
|
|
6134
|
+
* - resetAll: () => void — reset all selections and re-fetch initial options
|
|
6135
|
+
* - resetFilter: (paramName) => void — reset a single filter and its downstream filters
|
|
6136
|
+
*/
|
|
6137
|
+
const useCrossFilterCascading = ({ filters = [], fetchOptionsFn, initialSelections = {}, fetchOnMount = true, onSelectionChange, }) => {
|
|
6138
|
+
// State maps
|
|
6139
|
+
const [optionsMap, setOptionsMap] = React.useState({});
|
|
6140
|
+
const [loadingMap, setLoadingMap] = React.useState({});
|
|
6141
|
+
const [selectionsMap, setSelectionsMap] = React.useState(() => {
|
|
6142
|
+
// Initialize from initialSelections
|
|
6143
|
+
const initial = {};
|
|
6144
|
+
filters.forEach((f) => {
|
|
6145
|
+
const paramName = f.paramName || f.param_name || f.column_name || f.attribute_name;
|
|
6146
|
+
initial[paramName] = initialSelections[paramName] || [];
|
|
6147
|
+
});
|
|
6148
|
+
return initial;
|
|
6149
|
+
});
|
|
6150
|
+
// Refs for latest state access in async operations
|
|
6151
|
+
const selectionsRef = React.useRef(selectionsMap);
|
|
6152
|
+
const filtersRef = React.useRef(filters);
|
|
6153
|
+
const fetchFnRef = React.useRef(fetchOptionsFn);
|
|
6154
|
+
const mountedRef = React.useRef(true);
|
|
6155
|
+
// Keep refs in sync
|
|
6156
|
+
React.useEffect(() => {
|
|
6157
|
+
selectionsRef.current = selectionsMap;
|
|
6158
|
+
}, [selectionsMap]);
|
|
6159
|
+
React.useEffect(() => {
|
|
6160
|
+
filtersRef.current = filters;
|
|
6161
|
+
}, [filters]);
|
|
6162
|
+
React.useEffect(() => {
|
|
6163
|
+
fetchFnRef.current = fetchOptionsFn;
|
|
6164
|
+
}, [fetchOptionsFn]);
|
|
6165
|
+
React.useEffect(() => {
|
|
6166
|
+
return () => {
|
|
6167
|
+
mountedRef.current = false;
|
|
6168
|
+
};
|
|
6169
|
+
}, []);
|
|
6170
|
+
/**
|
|
6171
|
+
* Get the paramName from a filter config object (supports multiple naming conventions)
|
|
6172
|
+
*/
|
|
6173
|
+
const getParamName = React.useCallback((filter) => {
|
|
6174
|
+
return filter.paramName || filter.param_name || filter.column_name || filter.attribute_name;
|
|
6175
|
+
}, []);
|
|
6176
|
+
/**
|
|
6177
|
+
* Get the index of a filter in the hierarchy by its paramName
|
|
6178
|
+
*/
|
|
6179
|
+
const getFilterIndex = React.useCallback((paramName) => {
|
|
6180
|
+
return filtersRef.current.findIndex((f) => getParamName(f) === paramName);
|
|
6181
|
+
}, [getParamName]);
|
|
6182
|
+
/**
|
|
6183
|
+
* Get all filters that are downstream (higher index) of a given filter
|
|
6184
|
+
*/
|
|
6185
|
+
const getDownstreamFilters = React.useCallback((paramName) => {
|
|
6186
|
+
const index = getFilterIndex(paramName);
|
|
6187
|
+
if (index === -1)
|
|
6188
|
+
return [];
|
|
6189
|
+
return filtersRef.current.slice(index + 1);
|
|
6190
|
+
}, [getFilterIndex]);
|
|
6191
|
+
/**
|
|
6192
|
+
* Get all filters that are upstream (lower index) of a given filter
|
|
6193
|
+
*/
|
|
6194
|
+
const getUpstreamFilters = React.useCallback((paramName) => {
|
|
6195
|
+
const index = getFilterIndex(paramName);
|
|
6196
|
+
if (index <= 0)
|
|
6197
|
+
return [];
|
|
6198
|
+
return filtersRef.current.slice(0, index);
|
|
6199
|
+
}, [getFilterIndex]);
|
|
6200
|
+
/**
|
|
6201
|
+
* Build the existing selections payload for upstream filters (for cascading API call)
|
|
6202
|
+
*/
|
|
6203
|
+
const buildExistingSelections = React.useCallback((paramName) => {
|
|
6204
|
+
const upstreamFilters = getUpstreamFilters(paramName);
|
|
6205
|
+
const currentSelections = selectionsRef.current;
|
|
6206
|
+
return upstreamFilters
|
|
6207
|
+
.filter((f) => {
|
|
6208
|
+
const pName = getParamName(f);
|
|
6209
|
+
const vals = currentSelections[pName];
|
|
6210
|
+
return vals && vals.length > 0;
|
|
6211
|
+
})
|
|
6212
|
+
.map((f) => {
|
|
6213
|
+
const pName = getParamName(f);
|
|
6214
|
+
return {
|
|
6215
|
+
filterName: f.label || f.name || pName,
|
|
6216
|
+
attributeName: f.column_name || f.attribute_name || pName,
|
|
6217
|
+
values: currentSelections[pName] || [],
|
|
6218
|
+
checkAll: false,
|
|
6219
|
+
};
|
|
6220
|
+
});
|
|
6221
|
+
}, [getUpstreamFilters, getParamName]);
|
|
6222
|
+
/**
|
|
6223
|
+
* Fetch options for a single filter
|
|
6224
|
+
*/
|
|
6225
|
+
const fetchOptionsForFilter = React.useCallback(async (filterConfig) => {
|
|
6226
|
+
const paramName = getParamName(filterConfig);
|
|
6227
|
+
if (!fetchFnRef.current)
|
|
6228
|
+
return;
|
|
6229
|
+
// Set loading
|
|
6230
|
+
setLoadingMap((prev) => ({ ...prev, [paramName]: true }));
|
|
6231
|
+
try {
|
|
6232
|
+
const existingSelections = buildExistingSelections(paramName);
|
|
6233
|
+
const options = await fetchFnRef.current(filterConfig, existingSelections, filtersRef.current);
|
|
6234
|
+
if (!mountedRef.current)
|
|
6235
|
+
return;
|
|
6236
|
+
// Check if the response contains extra options for other filters
|
|
6237
|
+
const extraOptionsMap = options?.__extraOptionsMap;
|
|
6238
|
+
if (extraOptionsMap && typeof extraOptionsMap === "object") {
|
|
6239
|
+
// Distribute extra options to matching filters
|
|
6240
|
+
setOptionsMap((prev) => {
|
|
6241
|
+
const updated = { ...prev, [paramName]: options || [] };
|
|
6242
|
+
Object.keys(extraOptionsMap).forEach((key) => {
|
|
6243
|
+
// Only populate if this key matches a known filter in our hierarchy
|
|
6244
|
+
const matchingFilter = filtersRef.current.find((f) => getParamName(f) === key || f.column_name === key || f.attribute_name === key);
|
|
6245
|
+
if (matchingFilter) {
|
|
6246
|
+
const matchParamName = getParamName(matchingFilter);
|
|
6247
|
+
updated[matchParamName] = extraOptionsMap[key];
|
|
6248
|
+
}
|
|
6249
|
+
});
|
|
6250
|
+
return updated;
|
|
6251
|
+
});
|
|
6252
|
+
// Clear loading for extra filters that were populated
|
|
6253
|
+
setLoadingMap((prev) => {
|
|
6254
|
+
const updated = { ...prev, [paramName]: false };
|
|
6255
|
+
Object.keys(extraOptionsMap).forEach((key) => {
|
|
6256
|
+
const matchingFilter = filtersRef.current.find((f) => getParamName(f) === key || f.column_name === key || f.attribute_name === key);
|
|
6257
|
+
if (matchingFilter) {
|
|
6258
|
+
updated[getParamName(matchingFilter)] = false;
|
|
6259
|
+
}
|
|
6260
|
+
});
|
|
6261
|
+
return updated;
|
|
6262
|
+
});
|
|
6263
|
+
}
|
|
6264
|
+
else {
|
|
6265
|
+
setOptionsMap((prev) => ({ ...prev, [paramName]: options || [] }));
|
|
6266
|
+
setLoadingMap((prev) => ({ ...prev, [paramName]: false }));
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
catch (error) {
|
|
6270
|
+
console.error(`[useCrossFilterCascading] Error fetching options for ${paramName}:`, error);
|
|
6271
|
+
if (mountedRef.current) {
|
|
6272
|
+
setOptionsMap((prev) => ({ ...prev, [paramName]: [] }));
|
|
6273
|
+
setLoadingMap((prev) => ({ ...prev, [paramName]: false }));
|
|
6274
|
+
}
|
|
6275
|
+
}
|
|
6276
|
+
}, [getParamName, buildExistingSelections]);
|
|
6277
|
+
/**
|
|
6278
|
+
* Fetch options for multiple filters (used for initial load and cascade refresh)
|
|
6279
|
+
*/
|
|
6280
|
+
const fetchOptionsForFilters = React.useCallback(async (filterConfigs) => {
|
|
6281
|
+
await Promise.all(filterConfigs.map((f) => fetchOptionsForFilter(f)));
|
|
6282
|
+
}, [fetchOptionsForFilter]);
|
|
6283
|
+
/**
|
|
6284
|
+
* Handle a filter value change — triggers cascading for downstream filters
|
|
6285
|
+
*/
|
|
6286
|
+
const onFilterChange = React.useCallback((paramName, selectedValues) => {
|
|
6287
|
+
const values = Array.isArray(selectedValues) ? selectedValues : [selectedValues];
|
|
6288
|
+
// Update selections
|
|
6289
|
+
setSelectionsMap((prev) => {
|
|
6290
|
+
const updated = { ...prev, [paramName]: values };
|
|
6291
|
+
selectionsRef.current = updated;
|
|
6292
|
+
// Notify consumer
|
|
6293
|
+
if (onSelectionChange) {
|
|
6294
|
+
onSelectionChange(paramName, values, updated);
|
|
6295
|
+
}
|
|
6296
|
+
return updated;
|
|
6297
|
+
});
|
|
6298
|
+
// Only clear downstream if the filter is being emptied (user cleared the filter).
|
|
6299
|
+
// When a value is being selected/changed while downstream filters already have
|
|
6300
|
+
// selections, preserve them — the dropdown close handler will decide whether
|
|
6301
|
+
// to refetch downstream options.
|
|
6302
|
+
if (values.length === 0) {
|
|
6303
|
+
const downstreamFilters = getDownstreamFilters(paramName);
|
|
6304
|
+
if (downstreamFilters.length > 0) {
|
|
6305
|
+
// Clear downstream selections
|
|
6306
|
+
setSelectionsMap((prev) => {
|
|
6307
|
+
const updated = { ...prev };
|
|
6308
|
+
downstreamFilters.forEach((f) => {
|
|
6309
|
+
const pName = getParamName(f);
|
|
6310
|
+
updated[pName] = [];
|
|
6311
|
+
});
|
|
6312
|
+
selectionsRef.current = updated;
|
|
6313
|
+
return updated;
|
|
6314
|
+
});
|
|
6315
|
+
// Clear downstream options
|
|
6316
|
+
const downstreamParamNames = downstreamFilters.map(getParamName);
|
|
6317
|
+
setOptionsMap((prev) => {
|
|
6318
|
+
const updated = { ...prev };
|
|
6319
|
+
downstreamParamNames.forEach((pName) => {
|
|
6320
|
+
updated[pName] = [];
|
|
6321
|
+
});
|
|
6322
|
+
return updated;
|
|
6323
|
+
});
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
}, [getDownstreamFilters, getParamName, fetchOptionsForFilter, onSelectionChange]);
|
|
6327
|
+
/**
|
|
6328
|
+
* Reset all filters — clears selections and re-fetches initial options
|
|
6329
|
+
*/
|
|
6330
|
+
const resetAll = React.useCallback(() => {
|
|
6331
|
+
const initial = {};
|
|
6332
|
+
filtersRef.current.forEach((f) => {
|
|
6333
|
+
initial[getParamName(f)] = [];
|
|
6334
|
+
});
|
|
6335
|
+
setSelectionsMap(initial);
|
|
6336
|
+
selectionsRef.current = initial;
|
|
6337
|
+
setOptionsMap({});
|
|
6338
|
+
if (fetchFnRef.current && filtersRef.current.length > 0) {
|
|
6339
|
+
// Fetch options for the first filter (others will cascade)
|
|
6340
|
+
fetchOptionsForFilter(filtersRef.current[0]);
|
|
6341
|
+
}
|
|
6342
|
+
}, [getParamName, fetchOptionsForFilter]);
|
|
6343
|
+
/**
|
|
6344
|
+
* Reset a specific filter and all its downstream filters
|
|
6345
|
+
*/
|
|
6346
|
+
const resetFilter = React.useCallback((paramName) => {
|
|
6347
|
+
const downstreamFilters = getDownstreamFilters(paramName);
|
|
6348
|
+
const allToReset = [paramName, ...downstreamFilters.map(getParamName)];
|
|
6349
|
+
setSelectionsMap((prev) => {
|
|
6350
|
+
const updated = { ...prev };
|
|
6351
|
+
allToReset.forEach((pName) => {
|
|
6352
|
+
updated[pName] = [];
|
|
6353
|
+
});
|
|
6354
|
+
selectionsRef.current = updated;
|
|
6355
|
+
return updated;
|
|
6356
|
+
});
|
|
6357
|
+
// Re-fetch options for the reset filter itself
|
|
6358
|
+
const filterConfig = filtersRef.current.find((f) => getParamName(f) === paramName);
|
|
6359
|
+
if (filterConfig) {
|
|
6360
|
+
fetchOptionsForFilter(filterConfig);
|
|
6361
|
+
}
|
|
6362
|
+
}, [getDownstreamFilters, getParamName, fetchOptionsForFilter]);
|
|
6363
|
+
/**
|
|
6364
|
+
* Fetch options for a specific filter on demand (e.g., when dropdown is opened)
|
|
6365
|
+
*/
|
|
6366
|
+
const fetchOptions = React.useCallback((paramName) => {
|
|
6367
|
+
const filterConfig = filtersRef.current.find((f) => getParamName(f) === paramName);
|
|
6368
|
+
if (filterConfig) {
|
|
6369
|
+
fetchOptionsForFilter(filterConfig);
|
|
6370
|
+
}
|
|
6371
|
+
}, [getParamName, fetchOptionsForFilter]);
|
|
6372
|
+
/**
|
|
6373
|
+
* Fetch options for all downstream filters of a given filter.
|
|
6374
|
+
* Called when the user closes a dropdown after making a selection,
|
|
6375
|
+
* so downstream filters are pre-populated before the user opens them.
|
|
6376
|
+
*/
|
|
6377
|
+
const fetchDownstreamOptions = React.useCallback((paramName) => {
|
|
6378
|
+
const downstreamFilters = getDownstreamFilters(paramName);
|
|
6379
|
+
if (downstreamFilters.length > 0) {
|
|
6380
|
+
fetchOptionsForFilters(downstreamFilters);
|
|
6381
|
+
}
|
|
6382
|
+
}, [getDownstreamFilters, fetchOptionsForFilters]);
|
|
6383
|
+
/**
|
|
6384
|
+
* Build selections payload that includes the triggering filter and all other
|
|
6385
|
+
* filters that have selections (used for upstream/reverse cascading).
|
|
6386
|
+
*/
|
|
6387
|
+
const buildAllSelections = React.useCallback((excludeParamName) => {
|
|
6388
|
+
const currentSelections = selectionsRef.current;
|
|
6389
|
+
return filtersRef.current
|
|
6390
|
+
.filter((f) => {
|
|
6391
|
+
const pName = getParamName(f);
|
|
6392
|
+
if (pName === excludeParamName)
|
|
6393
|
+
return false;
|
|
6394
|
+
const vals = currentSelections[pName];
|
|
6395
|
+
return vals && vals.length > 0;
|
|
6396
|
+
})
|
|
6397
|
+
.map((f) => {
|
|
6398
|
+
const pName = getParamName(f);
|
|
6399
|
+
return {
|
|
6400
|
+
filterName: f.label || f.name || pName,
|
|
6401
|
+
attributeName: f.column_name || f.attribute_name || pName,
|
|
6402
|
+
values: currentSelections[pName] || [],
|
|
6403
|
+
checkAll: false,
|
|
6404
|
+
};
|
|
6405
|
+
});
|
|
6406
|
+
}, [getParamName]);
|
|
6407
|
+
/**
|
|
6408
|
+
* Fetch options for a single upstream filter using all current selections
|
|
6409
|
+
* (reverse cascading — the payload includes the triggering lower filter's values).
|
|
6410
|
+
*/
|
|
6411
|
+
const fetchUpstreamFilterOptions = React.useCallback(async (filterConfig) => {
|
|
6412
|
+
const paramName = getParamName(filterConfig);
|
|
6413
|
+
if (!fetchFnRef.current)
|
|
6414
|
+
return;
|
|
6415
|
+
setLoadingMap((prev) => ({ ...prev, [paramName]: true }));
|
|
6416
|
+
try {
|
|
6417
|
+
const existingSelections = buildAllSelections(paramName);
|
|
6418
|
+
const options = await fetchFnRef.current(filterConfig, existingSelections, filtersRef.current);
|
|
6419
|
+
if (!mountedRef.current)
|
|
6420
|
+
return;
|
|
6421
|
+
// Handle extra options from multi-key response
|
|
6422
|
+
const extraOptionsMap = options?.__extraOptionsMap;
|
|
6423
|
+
if (extraOptionsMap && typeof extraOptionsMap === "object") {
|
|
6424
|
+
setOptionsMap((prev) => {
|
|
6425
|
+
const updated = { ...prev, [paramName]: options || [] };
|
|
6426
|
+
Object.keys(extraOptionsMap).forEach((key) => {
|
|
6427
|
+
const matchingFilter = filtersRef.current.find((f) => getParamName(f) === key || f.column_name === key || f.attribute_name === key);
|
|
6428
|
+
if (matchingFilter) {
|
|
6429
|
+
updated[getParamName(matchingFilter)] = extraOptionsMap[key];
|
|
6430
|
+
}
|
|
6431
|
+
});
|
|
6432
|
+
return updated;
|
|
6433
|
+
});
|
|
6434
|
+
setLoadingMap((prev) => {
|
|
6435
|
+
const updated = { ...prev, [paramName]: false };
|
|
6436
|
+
Object.keys(extraOptionsMap).forEach((key) => {
|
|
6437
|
+
const matchingFilter = filtersRef.current.find((f) => getParamName(f) === key || f.column_name === key || f.attribute_name === key);
|
|
6438
|
+
if (matchingFilter) {
|
|
6439
|
+
updated[getParamName(matchingFilter)] = false;
|
|
6440
|
+
}
|
|
6441
|
+
});
|
|
6442
|
+
return updated;
|
|
6443
|
+
});
|
|
6444
|
+
}
|
|
6445
|
+
else {
|
|
6446
|
+
setOptionsMap((prev) => ({ ...prev, [paramName]: options || [] }));
|
|
6447
|
+
setLoadingMap((prev) => ({ ...prev, [paramName]: false }));
|
|
6448
|
+
}
|
|
6449
|
+
}
|
|
6450
|
+
catch (error) {
|
|
6451
|
+
console.error(`[useCrossFilterCascading] Error fetching upstream options for ${paramName}:`, error);
|
|
6452
|
+
if (mountedRef.current) {
|
|
6453
|
+
setOptionsMap((prev) => ({ ...prev, [paramName]: [] }));
|
|
6454
|
+
setLoadingMap((prev) => ({ ...prev, [paramName]: false }));
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
}, [getParamName, buildAllSelections]);
|
|
6458
|
+
/**
|
|
6459
|
+
* Fetch options for all upstream filters of a given filter.
|
|
6460
|
+
* Called on dropdown close to reverse-cascade — e.g., selecting Channel
|
|
6461
|
+
* triggers a fetch for Brand with Channel's selection in the payload.
|
|
6462
|
+
*/
|
|
6463
|
+
const fetchUpstreamOptions = React.useCallback((paramName) => {
|
|
6464
|
+
const upstreamFilters = getUpstreamFilters(paramName);
|
|
6465
|
+
if (upstreamFilters.length > 0) {
|
|
6466
|
+
Promise.all(upstreamFilters.map((f) => fetchUpstreamFilterOptions(f)));
|
|
6467
|
+
}
|
|
6468
|
+
}, [getUpstreamFilters, fetchUpstreamFilterOptions]);
|
|
6469
|
+
// Fetch initial options on mount
|
|
6470
|
+
React.useEffect(() => {
|
|
6471
|
+
if (fetchOnMount && filters.length > 0 && fetchFnRef.current) {
|
|
6472
|
+
// Fetch options for the first filter (it has no upstream dependencies)
|
|
6473
|
+
fetchOptionsForFilter(filters[0]);
|
|
6474
|
+
}
|
|
6475
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
6476
|
+
}, []);
|
|
6477
|
+
return {
|
|
6478
|
+
optionsMap,
|
|
6479
|
+
loadingMap,
|
|
6480
|
+
selectionsMap,
|
|
6481
|
+
onFilterChange,
|
|
6482
|
+
resetAll,
|
|
6483
|
+
resetFilter,
|
|
6484
|
+
fetchOptions,
|
|
6485
|
+
fetchDownstreamOptions,
|
|
6486
|
+
fetchUpstreamOptions,
|
|
6487
|
+
getParamName,
|
|
6488
|
+
};
|
|
6489
|
+
};
|
|
6490
|
+
|
|
6491
|
+
/**
|
|
6492
|
+
* Context for cross-filter cascading.
|
|
6493
|
+
* Provides cascading state and actions to child select components.
|
|
6494
|
+
*
|
|
6495
|
+
* Shape:
|
|
6496
|
+
* {
|
|
6497
|
+
* optionsMap: { paramName: Array<{label, value}> },
|
|
6498
|
+
* loadingMap: { paramName: boolean },
|
|
6499
|
+
* selectionsMap: { paramName: value[] },
|
|
6500
|
+
* onFilterChange: (paramName, selectedValues) => void,
|
|
6501
|
+
* resetAll: () => void,
|
|
6502
|
+
* resetFilter: (paramName) => void,
|
|
6503
|
+
* fetchOptions: (paramName) => void,
|
|
6504
|
+
* getParamName: (filterConfig) => string,
|
|
6505
|
+
* isEnabled: boolean, // flag to know if cascading is active
|
|
6506
|
+
* }
|
|
6507
|
+
*/
|
|
6508
|
+
const CrossFilterContext = React.createContext(null);
|
|
6509
|
+
/**
|
|
6510
|
+
* Hook to consume cross-filter cascading context.
|
|
6511
|
+
* Returns null if not inside a CrossFilterProvider (graceful fallback).
|
|
6512
|
+
*/
|
|
6513
|
+
const useCrossFilterContext = () => {
|
|
6514
|
+
return React.useContext(CrossFilterContext);
|
|
6515
|
+
};
|
|
6516
|
+
|
|
6517
|
+
/**
|
|
6518
|
+
* Provider component that wraps a group of select components to enable
|
|
6519
|
+
* cross-filter cascading between them.
|
|
6520
|
+
*
|
|
6521
|
+
* @param {Object} props
|
|
6522
|
+
* @param {Array} props.filters - Array of filter configs (ordered by hierarchy)
|
|
6523
|
+
* @param {Function} props.fetchOptionsFn - Async function to fetch options for a filter
|
|
6524
|
+
* @param {Object} props.initialSelections - Optional preloaded selections { paramName: values[] }
|
|
6525
|
+
* @param {boolean} props.fetchOnMount - Whether to fetch initial options on mount (default: true)
|
|
6526
|
+
* @param {Function} props.onSelectionChange - Optional callback on any filter change
|
|
6527
|
+
* @param {React.ReactNode} props.children - Child components (selects) that will consume the context
|
|
6528
|
+
*/
|
|
6529
|
+
const CrossFilterProvider = ({ filters, fetchOptionsFn, initialSelections = {}, fetchOnMount = true, onSelectionChange = null, children, }) => {
|
|
6530
|
+
const cascadingState = useCrossFilterCascading({
|
|
6531
|
+
filters,
|
|
6532
|
+
fetchOptionsFn,
|
|
6533
|
+
initialSelections,
|
|
6534
|
+
fetchOnMount,
|
|
6535
|
+
onSelectionChange,
|
|
6536
|
+
});
|
|
6537
|
+
const contextValue = React.useMemo(() => ({
|
|
6538
|
+
...cascadingState,
|
|
6539
|
+
isEnabled: true,
|
|
6540
|
+
}), [cascadingState]);
|
|
6541
|
+
return (jsxRuntime.jsx(CrossFilterContext.Provider, { value: contextValue, children: children }));
|
|
6542
|
+
};
|
|
6543
|
+
|
|
6544
|
+
/**
|
|
6545
|
+
* Adapter function that bridges the generic useCrossFilterCascading hook
|
|
6546
|
+
* with the existing cross-filter API used by the chatbot.
|
|
6547
|
+
*
|
|
6548
|
+
* This is the `fetchOptionsFn` passed to the CrossFilterProvider.
|
|
6549
|
+
*
|
|
6550
|
+
* @param {Object} filterConfig - The filter config for which to fetch options
|
|
6551
|
+
* @param {Array} existingSelections - Upstream filter selections
|
|
6552
|
+
* Each item: { filterName, attributeName, values, checkAll }
|
|
6553
|
+
* @param {Array} allFilters - Full array of all filter configs
|
|
6554
|
+
* @returns {Promise<Array<{label, value}>>} - Formatted options
|
|
6555
|
+
*/
|
|
6556
|
+
const fetchCrossFilterOptions = async (filterConfig, existingSelections = [], allFilters = []) => {
|
|
6557
|
+
try {
|
|
6558
|
+
const attributeName = filterConfig.column_name ||
|
|
6559
|
+
filterConfig.attribute_name ||
|
|
6560
|
+
filterConfig.param_name ||
|
|
6561
|
+
filterConfig.paramName;
|
|
6562
|
+
if (!attributeName)
|
|
6563
|
+
return [];
|
|
6564
|
+
const dimension = filterConfig.dimension || "product";
|
|
6565
|
+
// Build filters array from existing selections (upstream filters with values)
|
|
6566
|
+
const filtersArray = existingSelections.map((selection) => {
|
|
6567
|
+
// Find full config for this upstream filter
|
|
6568
|
+
const fullConfig = allFilters.find((f) => (f.column_name || f.attribute_name || f.param_name || f.paramName) === selection.attributeName);
|
|
6569
|
+
const selAttrName = fullConfig?.column_name ||
|
|
6570
|
+
fullConfig?.attribute_name ||
|
|
6571
|
+
fullConfig?.param_name ||
|
|
6572
|
+
fullConfig?.paramName ||
|
|
6573
|
+
selection.attributeName;
|
|
6574
|
+
return {
|
|
6575
|
+
filter_name: selection.filterName || selAttrName,
|
|
6576
|
+
filter_id: selAttrName,
|
|
6577
|
+
filter_type: "cascaded",
|
|
6578
|
+
dimension: fullConfig?.dimension || dimension,
|
|
6579
|
+
display_type: fullConfig?.display_type || "dropdown",
|
|
6580
|
+
check_configuration: selection.checkAll ? [{ checkAll: true, meta: {} }] : [],
|
|
6581
|
+
is_mandatory: fullConfig?.is_mandatory || fullConfig?.isRequired || false,
|
|
6582
|
+
extra: {},
|
|
6583
|
+
values: selection.checkAll ? [] : (selection.values || []),
|
|
6584
|
+
attribute_name: selAttrName,
|
|
6585
|
+
operator: "in",
|
|
6586
|
+
display_order: fullConfig?.display_order || fullConfig?.ordering || 0,
|
|
6587
|
+
};
|
|
6588
|
+
});
|
|
6589
|
+
const payload = {
|
|
6590
|
+
attributes: [
|
|
6591
|
+
{
|
|
6592
|
+
attribute_name: attributeName,
|
|
6593
|
+
dimension: dimension,
|
|
6594
|
+
filter_type: "cascaded",
|
|
6595
|
+
},
|
|
6596
|
+
],
|
|
6597
|
+
filter_type: "cascaded",
|
|
6598
|
+
filters: filtersArray,
|
|
6599
|
+
is_urm_filter: true,
|
|
6600
|
+
screen_name: "Chatbot",
|
|
6601
|
+
application_code: 1,
|
|
6602
|
+
};
|
|
6603
|
+
console.log("[crossFilterAdapter] Fetching options for:", attributeName, "payload:", JSON.stringify(payload, null, 2));
|
|
6604
|
+
const response = await getFilterOptions(payload)();
|
|
6605
|
+
console.log("[crossFilterAdapter] Response for:", attributeName, "data:", response?.data?.data);
|
|
6606
|
+
if (response?.data?.status && response?.data?.data) {
|
|
6607
|
+
const responseData = response.data.data;
|
|
6608
|
+
const primaryValues = responseData[attributeName];
|
|
6609
|
+
// Format the primary filter's options
|
|
6610
|
+
let primaryOptions = Array.isArray(primaryValues)
|
|
6611
|
+
? primaryValues.map((value) => {
|
|
6612
|
+
const stringValue = String(value);
|
|
6613
|
+
return {
|
|
6614
|
+
label: replaceSpecialCharacter(stringValue),
|
|
6615
|
+
value: stringValue,
|
|
6616
|
+
};
|
|
6617
|
+
})
|
|
6618
|
+
: [];
|
|
6619
|
+
// Check if the response contains options for other filters (multi-key response)
|
|
6620
|
+
const extraOptionsMap = {};
|
|
6621
|
+
Object.keys(responseData).forEach((key) => {
|
|
6622
|
+
if (key !== attributeName && Array.isArray(responseData[key])) {
|
|
6623
|
+
extraOptionsMap[key] = responseData[key].map((value) => {
|
|
6624
|
+
const stringValue = String(value);
|
|
6625
|
+
return {
|
|
6626
|
+
label: replaceSpecialCharacter(stringValue),
|
|
6627
|
+
value: stringValue,
|
|
6628
|
+
};
|
|
6629
|
+
});
|
|
6630
|
+
}
|
|
6631
|
+
});
|
|
6632
|
+
// If primary options are empty but response has data under other keys,
|
|
6633
|
+
// include ALL response keys in extraOptionsMap so the hook can distribute
|
|
6634
|
+
// them to matching filters by column_name/attribute_name lookup.
|
|
6635
|
+
if (primaryOptions.length === 0 && Object.keys(extraOptionsMap).length > 0) {
|
|
6636
|
+
// Also check if any response key matches the paramName directly
|
|
6637
|
+
// (e.g., paramName is "brand" but column_name sent was different)
|
|
6638
|
+
const altKey = Object.keys(responseData).find((key) => {
|
|
6639
|
+
return Array.isArray(responseData[key]) && (key === attributeName ||
|
|
6640
|
+
key.toLowerCase() === attributeName.toLowerCase());
|
|
6641
|
+
});
|
|
6642
|
+
if (altKey) {
|
|
6643
|
+
primaryOptions = responseData[altKey].map((value) => {
|
|
6644
|
+
const stringValue = String(value);
|
|
6645
|
+
return {
|
|
6646
|
+
label: replaceSpecialCharacter(stringValue),
|
|
6647
|
+
value: stringValue,
|
|
6648
|
+
};
|
|
6649
|
+
});
|
|
6650
|
+
delete extraOptionsMap[altKey];
|
|
6651
|
+
}
|
|
6652
|
+
}
|
|
6653
|
+
// Return enriched result with extra options if available
|
|
6654
|
+
if (Object.keys(extraOptionsMap).length > 0) {
|
|
6655
|
+
primaryOptions.__extraOptionsMap = extraOptionsMap;
|
|
6656
|
+
}
|
|
6657
|
+
return primaryOptions;
|
|
6658
|
+
}
|
|
6659
|
+
return [];
|
|
6660
|
+
}
|
|
6661
|
+
catch (error) {
|
|
6662
|
+
console.error("[crossFilterAdapter] Error fetching options:", error);
|
|
6663
|
+
return [];
|
|
6664
|
+
}
|
|
6665
|
+
};
|
|
6666
|
+
|
|
6069
6667
|
const SliderContent = ({ bodyText, isFormDisabled = false, messageIndex }) => {
|
|
6070
6668
|
const formKey = `${messageIndex}_${bodyText?.paramName}`;
|
|
6071
6669
|
const { header, headerOrentiation, inputPosition, label, max, min, required, disabled, } = bodyText;
|
|
@@ -6115,6 +6713,10 @@ const SelectContent = ({ bodyText, isFormDisabled = false, messageIndex }) => {
|
|
|
6115
6713
|
const chatbotContext = reactRedux.useSelector((state) => state.smartBotReducer.chatbotContext);
|
|
6116
6714
|
const heirarchyKeyValuePairs = reactRedux.useSelector((state) => state.smartBotReducer.heirarchyKeyValuePairs);
|
|
6117
6715
|
const dispatch = reactRedux.useDispatch();
|
|
6716
|
+
// Cross-filter cascading context (optional — graceful fallback when not wrapped)
|
|
6717
|
+
const crossFilterCtx = useCrossFilterContext();
|
|
6718
|
+
const isCascading = crossFilterCtx?.isEnabled && paramName;
|
|
6719
|
+
const isMountedRef = React.useRef(false);
|
|
6118
6720
|
if (lodash.isEmpty(bodyText))
|
|
6119
6721
|
return null;
|
|
6120
6722
|
const onChange = (selectedOptions) => {
|
|
@@ -6138,6 +6740,10 @@ const SelectContent = ({ bodyText, isFormDisabled = false, messageIndex }) => {
|
|
|
6138
6740
|
// };
|
|
6139
6741
|
dispatch(smartBotActions.setChatbotContext(chatbotContext));
|
|
6140
6742
|
dispatch(smartBotActions.setPersistedFormValues({ [formKey]: Array.isArray(selectedOptions) ? selectedOptions : [selectedOptions] }));
|
|
6743
|
+
// Notify cross-filter context if cascading is active
|
|
6744
|
+
if (isCascading) {
|
|
6745
|
+
crossFilterCtx.onFilterChange(paramName, Array.isArray(value) ? value : [value]);
|
|
6746
|
+
}
|
|
6141
6747
|
}
|
|
6142
6748
|
catch (error) {
|
|
6143
6749
|
console.error("Error in select handleChange", error);
|
|
@@ -6149,16 +6755,94 @@ const SelectContent = ({ bodyText, isFormDisabled = false, messageIndex }) => {
|
|
|
6149
6755
|
setCurrentSelectedOptions([]);
|
|
6150
6756
|
}
|
|
6151
6757
|
}, [persistedFormValues, formKey]);
|
|
6758
|
+
// When cross-filter context provides new options for this filter, update local state
|
|
6152
6759
|
React.useEffect(() => {
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6760
|
+
if (isCascading && crossFilterCtx.optionsMap[paramName]) {
|
|
6761
|
+
const cascadedOptions = crossFilterCtx.optionsMap[paramName];
|
|
6762
|
+
allOptionsRef.current = cascadedOptions;
|
|
6763
|
+
const initialSlice = formatSlice(cascadedOptions, 0, INITIAL_DISPLAY_COUNT);
|
|
6764
|
+
setInitialOptions(initialSlice);
|
|
6765
|
+
setCurrentOptions(initialSlice);
|
|
6766
|
+
}
|
|
6767
|
+
}, [isCascading, crossFilterCtx?.optionsMap?.[paramName]]);
|
|
6768
|
+
// When cascading resets downstream selections, clear local selection
|
|
6769
|
+
// Skip on initial mount to avoid clearing persisted values when context reinitializes
|
|
6770
|
+
React.useEffect(() => {
|
|
6771
|
+
if (!isMountedRef.current) {
|
|
6772
|
+
isMountedRef.current = true;
|
|
6773
|
+
return;
|
|
6774
|
+
}
|
|
6775
|
+
if (isCascading) {
|
|
6776
|
+
const cascadedSelection = crossFilterCtx.selectionsMap[paramName];
|
|
6777
|
+
if (cascadedSelection && cascadedSelection.length === 0 && currentSelectedOptions.length > 0) {
|
|
6778
|
+
setCurrentSelectedOptions([]);
|
|
6779
|
+
setIsAllSelected(false);
|
|
6780
|
+
// Also clear from chatbotContext and persistedFormValues
|
|
6781
|
+
chatbotContext[bodyText?.paramName] = {
|
|
6782
|
+
...chatbotContext?.[bodyText?.paramName],
|
|
6783
|
+
[bodyText?.paramName]: [],
|
|
6784
|
+
updated: true,
|
|
6785
|
+
};
|
|
6786
|
+
dispatch(smartBotActions.setChatbotContext(chatbotContext));
|
|
6787
|
+
dispatch(smartBotActions.setPersistedFormValues({ [formKey]: [] }));
|
|
6788
|
+
}
|
|
6789
|
+
}
|
|
6790
|
+
}, [isCascading, crossFilterCtx?.selectionsMap?.[paramName]]);
|
|
6791
|
+
React.useEffect(() => {
|
|
6792
|
+
// Only use static options from bodyText if NOT in cascading mode
|
|
6793
|
+
if (!isCascading) {
|
|
6794
|
+
allOptionsRef.current = options;
|
|
6795
|
+
const initialSlice = formatSlice(options, 0, INITIAL_DISPLAY_COUNT);
|
|
6796
|
+
setInitialOptions(initialSlice);
|
|
6797
|
+
setCurrentOptions(initialSlice);
|
|
6798
|
+
}
|
|
6799
|
+
}, [isCascading]);
|
|
6158
6800
|
return (jsxRuntime.jsx("div", { style: { width: "100%", marginTop: "10px" }, children: jsxRuntime.jsx(impactUiV3.Select, { currentOptions: currentOptions, setCurrentOptions: setCurrentOptions, label: heirarchyKeyValuePairs[paramName] || label, labelOrientation: labelOrientation,
|
|
6159
6801
|
// inputPosition={inputPosition}
|
|
6160
6802
|
// header={header}
|
|
6161
|
-
isRequired: isRequired, isDisabled: isDisabled || isFormDisabled,
|
|
6803
|
+
isRequired: isRequired, isDisabled: isDisabled || isFormDisabled, isClearable: !(isDisabled || isFormDisabled), onClearAll: () => {
|
|
6804
|
+
setCurrentSelectedOptions([]);
|
|
6805
|
+
setIsAllSelected(false);
|
|
6806
|
+
chatbotContext[bodyText?.paramName] = {
|
|
6807
|
+
...chatbotContext?.[bodyText?.paramName],
|
|
6808
|
+
[bodyText?.paramName]: [],
|
|
6809
|
+
updated: true,
|
|
6810
|
+
};
|
|
6811
|
+
dispatch(smartBotActions.setChatbotContext(chatbotContext));
|
|
6812
|
+
dispatch(smartBotActions.setPersistedFormValues({ [formKey]: [] }));
|
|
6813
|
+
// Notify cross-filter context to clear downstream filters
|
|
6814
|
+
if (isCascading) {
|
|
6815
|
+
crossFilterCtx.onFilterChange(paramName, []);
|
|
6816
|
+
}
|
|
6817
|
+
}, handleChange: (selected) => onChange(selected), isCloseWhenClickOutside: true, setIsOpen: (open) => {
|
|
6818
|
+
setIsOpen(open);
|
|
6819
|
+
if (isCascading) {
|
|
6820
|
+
if (open) {
|
|
6821
|
+
// Lazy fetch: trigger cross-filter API call when dropdown opens for the first time
|
|
6822
|
+
if ((!crossFilterCtx.optionsMap[paramName] || crossFilterCtx.optionsMap[paramName].length === 0) && !crossFilterCtx.loadingMap[paramName]) {
|
|
6823
|
+
crossFilterCtx.fetchOptions(paramName);
|
|
6824
|
+
}
|
|
6825
|
+
}
|
|
6826
|
+
else {
|
|
6827
|
+
// On dropdown close: if there's a selection, pre-fetch related filter options
|
|
6828
|
+
const currentSelection = crossFilterCtx.selectionsMap[paramName];
|
|
6829
|
+
if (currentSelection && currentSelection.length > 0) {
|
|
6830
|
+
// Only fetch downstream if no downstream filter already has selections
|
|
6831
|
+
// (avoids clearing user's existing lower-hierarchy choices)
|
|
6832
|
+
const hasDownstreamSelections = Object.keys(crossFilterCtx.selectionsMap).some((key) => {
|
|
6833
|
+
if (key === paramName)
|
|
6834
|
+
return false;
|
|
6835
|
+
const vals = crossFilterCtx.selectionsMap[key];
|
|
6836
|
+
return vals && vals.length > 0;
|
|
6837
|
+
});
|
|
6838
|
+
if (!hasDownstreamSelections) {
|
|
6839
|
+
crossFilterCtx.fetchDownstreamOptions(paramName);
|
|
6840
|
+
}
|
|
6841
|
+
crossFilterCtx.fetchUpstreamOptions(paramName);
|
|
6842
|
+
}
|
|
6843
|
+
}
|
|
6844
|
+
}
|
|
6845
|
+
}, 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: () => {
|
|
6162
6846
|
const allRaw = allOptionsRef.current;
|
|
6163
6847
|
if (allRaw.length > 0 && currentOptions.length < allRaw.length) {
|
|
6164
6848
|
const nextCount = Math.min(currentOptions.length + LOAD_MORE_COUNT, allRaw.length);
|
|
@@ -6182,10 +6866,18 @@ const SelectContent = ({ bodyText, isFormDisabled = false, messageIndex }) => {
|
|
|
6182
6866
|
};
|
|
6183
6867
|
dispatch(smartBotActions.setChatbotContext(chatbotContext));
|
|
6184
6868
|
dispatch(smartBotActions.setPersistedFormValues({ [formKey]: currentOptions }));
|
|
6869
|
+
// Notify cross-filter context if cascading is active
|
|
6870
|
+
if (isCascading) {
|
|
6871
|
+
crossFilterCtx.onFilterChange(paramName, allValues);
|
|
6872
|
+
}
|
|
6185
6873
|
}
|
|
6186
6874
|
else {
|
|
6187
6875
|
setCurrentSelectedOptions([]);
|
|
6188
6876
|
setIsAllSelected(false);
|
|
6877
|
+
// Notify cross-filter context of deselection
|
|
6878
|
+
if (isCascading) {
|
|
6879
|
+
crossFilterCtx.onFilterChange(paramName, []);
|
|
6880
|
+
}
|
|
6189
6881
|
}
|
|
6190
6882
|
}, customPlaceholderAfterSelect: isAllSelected && allOptionsRef.current.length > 0
|
|
6191
6883
|
? allOptionsRef.current.length
|
|
@@ -6399,6 +7091,8 @@ const StepFormContent = ({ formData, messageIndex = 0, isFormDisabled = false, s
|
|
|
6399
7091
|
const persistedFormValues = reactRedux.useSelector((state) => state.smartBotReducer.persistedFormValues);
|
|
6400
7092
|
const chatbotContext = reactRedux.useSelector((state) => state.smartBotReducer.chatbotContext);
|
|
6401
7093
|
const stepFormStreamData = reactRedux.useSelector((state) => state.smartBotReducer.stepFormStreamData);
|
|
7094
|
+
const chatbotFilterOptions = reactRedux.useSelector((state) => state.smartBotReducer.chatbotFilterOptions);
|
|
7095
|
+
const dimensionHierarchies = reactRedux.useSelector((state) => state.userRoleManagementService?.dimensionHierarchies);
|
|
6402
7096
|
const [isFormSubmitted, setIsFormSubmitted] = React.useState(false);
|
|
6403
7097
|
React.useEffect(() => {
|
|
6404
7098
|
if (stepFormStreamData?.status === "streaming_start") {
|
|
@@ -6505,6 +7199,77 @@ const StepFormContent = ({ formData, messageIndex = 0, isFormDisabled = false, s
|
|
|
6505
7199
|
return false;
|
|
6506
7200
|
});
|
|
6507
7201
|
}, [formData, chatbotContext, persistedFormValues, messageIndex]);
|
|
7202
|
+
// Extract select-type filter configs from formData for cross-filter cascading
|
|
7203
|
+
// and sort them based on the filters_hierarchy_order from tenant config
|
|
7204
|
+
const crossFilterConfigs = React.useMemo(() => {
|
|
7205
|
+
if (!formData || !Array.isArray(formData))
|
|
7206
|
+
return [];
|
|
7207
|
+
const configs = formData
|
|
7208
|
+
.filter((item) => item?.type === "select" && item?.data?.param_name)
|
|
7209
|
+
.map((item) => {
|
|
7210
|
+
const data = item.data;
|
|
7211
|
+
const paramName = data.param_name;
|
|
7212
|
+
// Try to find full filter config from chatbotFilterOptions (Redux)
|
|
7213
|
+
const fullConfig = (chatbotFilterOptions || []).find((f) => f.column_name === paramName || f.attribute_name === paramName || f.name === paramName);
|
|
7214
|
+
return {
|
|
7215
|
+
paramName: paramName,
|
|
7216
|
+
param_name: paramName,
|
|
7217
|
+
column_name: fullConfig?.column_name || paramName,
|
|
7218
|
+
attribute_name: fullConfig?.attribute_name || paramName,
|
|
7219
|
+
dimension: fullConfig?.dimension || "product",
|
|
7220
|
+
display_type: fullConfig?.display_type || "dropdown",
|
|
7221
|
+
display_order: fullConfig?.display_order || item.ordering || 0,
|
|
7222
|
+
label: data.label,
|
|
7223
|
+
name: fullConfig?.name || data.label,
|
|
7224
|
+
is_mandatory: data.isRequired || false,
|
|
7225
|
+
};
|
|
7226
|
+
});
|
|
7227
|
+
// Sort configs based on dimensionHierarchies order.
|
|
7228
|
+
// Filters are sorted by their position in their dimension's hierarchy list.
|
|
7229
|
+
// This determines the cascading order (earlier = upstream/higher hierarchy).
|
|
7230
|
+
if (dimensionHierarchies && Object.keys(dimensionHierarchies).length > 0) {
|
|
7231
|
+
configs.sort((a, b) => {
|
|
7232
|
+
const aHierarchy = dimensionHierarchies[a.dimension] || [];
|
|
7233
|
+
const bHierarchy = dimensionHierarchies[b.dimension] || [];
|
|
7234
|
+
const aIndex = aHierarchy.indexOf(a.column_name) !== -1
|
|
7235
|
+
? aHierarchy.indexOf(a.column_name)
|
|
7236
|
+
: aHierarchy.indexOf(a.attribute_name) !== -1
|
|
7237
|
+
? aHierarchy.indexOf(a.attribute_name)
|
|
7238
|
+
: aHierarchy.indexOf(a.paramName) !== -1
|
|
7239
|
+
? aHierarchy.indexOf(a.paramName)
|
|
7240
|
+
: 999;
|
|
7241
|
+
const bIndex = bHierarchy.indexOf(b.column_name) !== -1
|
|
7242
|
+
? bHierarchy.indexOf(b.column_name)
|
|
7243
|
+
: bHierarchy.indexOf(b.attribute_name) !== -1
|
|
7244
|
+
? bHierarchy.indexOf(b.attribute_name)
|
|
7245
|
+
: bHierarchy.indexOf(b.paramName) !== -1
|
|
7246
|
+
? bHierarchy.indexOf(b.paramName)
|
|
7247
|
+
: 999;
|
|
7248
|
+
return aIndex - bIndex;
|
|
7249
|
+
});
|
|
7250
|
+
}
|
|
7251
|
+
return configs;
|
|
7252
|
+
}, [formData, chatbotFilterOptions, dimensionHierarchies]);
|
|
7253
|
+
// Build initial selections from chatbotContext and persistedFormValues (for restore on remount)
|
|
7254
|
+
const crossFilterInitialSelections = React.useMemo(() => {
|
|
7255
|
+
const selections = {};
|
|
7256
|
+
crossFilterConfigs.forEach((cfg) => {
|
|
7257
|
+
const ctx = chatbotContext?.[cfg.paramName];
|
|
7258
|
+
if (ctx && ctx.updated && ctx[cfg.paramName]) {
|
|
7259
|
+
const val = ctx[cfg.paramName];
|
|
7260
|
+
selections[cfg.paramName] = Array.isArray(val) ? val : [val];
|
|
7261
|
+
}
|
|
7262
|
+
else {
|
|
7263
|
+
// Fallback: check persistedFormValues for option objects
|
|
7264
|
+
const persistedKey = `${messageIndex}_${cfg.paramName}`;
|
|
7265
|
+
const persisted = persistedFormValues?.[persistedKey];
|
|
7266
|
+
if (persisted && Array.isArray(persisted) && persisted.length > 0) {
|
|
7267
|
+
selections[cfg.paramName] = persisted.map((opt) => opt.value || opt);
|
|
7268
|
+
}
|
|
7269
|
+
}
|
|
7270
|
+
});
|
|
7271
|
+
return selections;
|
|
7272
|
+
}, [crossFilterConfigs, chatbotContext, persistedFormValues, messageIndex]);
|
|
6508
7273
|
if (!formData || !Array.isArray(formData) || formData.length === 0) {
|
|
6509
7274
|
return null;
|
|
6510
7275
|
}
|
|
@@ -6566,7 +7331,7 @@ const StepFormContent = ({ formData, messageIndex = 0, isFormDisabled = false, s
|
|
|
6566
7331
|
return null;
|
|
6567
7332
|
return (jsxRuntime.jsxs("div", { className: "step-form-content", children: [showSavedFilters && filterSetOptions.length > 0 && (jsxRuntime.jsx("div", { style: { width: "100%", marginTop: "10px" }, children: jsxRuntime.jsx(impactUiV3.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 && (jsxRuntime.jsx("hr", { style: { border: "none", borderTop: "1px solid #E0E0E0", margin: "12px 0" } })), jsxRuntime.jsx("div", { style: {
|
|
6568
7333
|
...(isFilterSelected && !isFormDisabled ? { pointerEvents: "none", opacity: 0.5 } : {}),
|
|
6569
|
-
}, children: formFields }), buttonItems] }));
|
|
7334
|
+
}, children: crossFilterConfigs.length > 1 ? (jsxRuntime.jsx(CrossFilterProvider, { filters: crossFilterConfigs, fetchOptionsFn: fetchCrossFilterOptions, initialSelections: crossFilterInitialSelections, fetchOnMount: false, children: formFields })) : (formFields) }), buttonItems] }));
|
|
6570
7335
|
};
|
|
6571
7336
|
|
|
6572
7337
|
const useStyles$4 = makeStyles((theme) => ({
|
|
@@ -7451,6 +8216,26 @@ const StreamedContent = ({ botData }) => {
|
|
|
7451
8216
|
}));
|
|
7452
8217
|
return;
|
|
7453
8218
|
}
|
|
8219
|
+
// Skip is_error chunks — already handled in AxiosEventSource sseevent()
|
|
8220
|
+
if (data?.is_error) {
|
|
8221
|
+
// Trigger completion if status is "completed"
|
|
8222
|
+
if (data?.status === "completed") {
|
|
8223
|
+
setStepsDone(true);
|
|
8224
|
+
setIsStreamingDone(true);
|
|
8225
|
+
const doneState = streamStateMap.get(streamKey);
|
|
8226
|
+
if (doneState)
|
|
8227
|
+
doneState.completed = true;
|
|
8228
|
+
dispatch(smartBotActions.setMinimizedStreamData({
|
|
8229
|
+
isStreaming: false,
|
|
8230
|
+
stepHeader: questionsRef.current[questionsRef.current.length - 1] || "Completed",
|
|
8231
|
+
stepSubHeader: "Done",
|
|
8232
|
+
stepStatus: "completed",
|
|
8233
|
+
streamStartTime: streamStartTimeRef.current,
|
|
8234
|
+
actionCount: questionsRef.current.length || 1,
|
|
8235
|
+
}));
|
|
8236
|
+
}
|
|
8237
|
+
return;
|
|
8238
|
+
}
|
|
7454
8239
|
if (data?.message || data?.status === "step" || data?.status === "step_form" || data?.status === "thinking" || data?.status === "questions" || data?.status === "widget") {
|
|
7455
8240
|
if (data.status === "questions") {
|
|
7456
8241
|
const incomingQuestions = data.widget_data?.[0]?.questions || [];
|
|
@@ -7747,9 +8532,26 @@ const StreamedContent = ({ botData }) => {
|
|
|
7747
8532
|
stepRef.current = [...currentSteps];
|
|
7748
8533
|
setSteps([...currentSteps]);
|
|
7749
8534
|
}
|
|
7750
|
-
//
|
|
8535
|
+
// Show the abrupt close message at the bottom of any existing content.
|
|
8536
|
+
// If widgets already exist in appendedData, append the error as the last widget
|
|
8537
|
+
// so it renders AFTER the already-received content (not at the top).
|
|
7751
8538
|
const abruptCloseMessage = "The requested Task has ended unexpectedly, please retry again";
|
|
7752
|
-
messageToStoreRef.current.
|
|
8539
|
+
const hasExistingWidgetsOnErr = messageToStoreRef.current.appendedData &&
|
|
8540
|
+
(isArray(messageToStoreRef.current.appendedData)
|
|
8541
|
+
? messageToStoreRef.current.appendedData.length > 0
|
|
8542
|
+
: Object.keys(messageToStoreRef.current.appendedData).length > 0);
|
|
8543
|
+
if (hasExistingWidgetsOnErr) {
|
|
8544
|
+
const errorWidget = { type: "text", response: abruptCloseMessage };
|
|
8545
|
+
if (isArray(messageToStoreRef.current.appendedData)) {
|
|
8546
|
+
messageToStoreRef.current.appendedData.push(errorWidget);
|
|
8547
|
+
}
|
|
8548
|
+
else {
|
|
8549
|
+
messageToStoreRef.current.appendedData = [messageToStoreRef.current.appendedData, errorWidget];
|
|
8550
|
+
}
|
|
8551
|
+
}
|
|
8552
|
+
else {
|
|
8553
|
+
messageToStoreRef.current.chatData.response = abruptCloseMessage;
|
|
8554
|
+
}
|
|
7753
8555
|
setContent(abruptCloseMessage);
|
|
7754
8556
|
// Dispatch minimized widget data
|
|
7755
8557
|
dispatch(smartBotActions.setMinimizedStreamData({
|
|
@@ -7787,9 +8589,30 @@ const StreamedContent = ({ botData }) => {
|
|
|
7787
8589
|
stepRef.current = [...currentSteps];
|
|
7788
8590
|
setSteps([...currentSteps]);
|
|
7789
8591
|
}
|
|
7790
|
-
//
|
|
8592
|
+
// Show the abrupt close message. The final response array is structured as:
|
|
8593
|
+
// [textResponseTobeParsed, ...appendedData] — so chatData.response renders FIRST,
|
|
8594
|
+
// then widget items from appendedData render below it.
|
|
8595
|
+
// To ensure the error message appears AFTER already-rendered widgets, we append it
|
|
8596
|
+
// as a text widget item to appendedData instead of setting it on chatData.response.
|
|
7791
8597
|
const abruptCloseMessage = "The requested Task has ended unexpectedly, please retry again";
|
|
7792
|
-
messageToStoreRef.current.
|
|
8598
|
+
const hasExistingWidgets = messageToStoreRef.current.appendedData &&
|
|
8599
|
+
(isArray(messageToStoreRef.current.appendedData)
|
|
8600
|
+
? messageToStoreRef.current.appendedData.length > 0
|
|
8601
|
+
: Object.keys(messageToStoreRef.current.appendedData).length > 0);
|
|
8602
|
+
if (hasExistingWidgets) {
|
|
8603
|
+
// Widgets already exist — append error as the last widget so it renders at the bottom
|
|
8604
|
+
const errorWidget = { type: "text", response: abruptCloseMessage };
|
|
8605
|
+
if (isArray(messageToStoreRef.current.appendedData)) {
|
|
8606
|
+
messageToStoreRef.current.appendedData.push(errorWidget);
|
|
8607
|
+
}
|
|
8608
|
+
else {
|
|
8609
|
+
messageToStoreRef.current.appendedData = [messageToStoreRef.current.appendedData, errorWidget];
|
|
8610
|
+
}
|
|
8611
|
+
}
|
|
8612
|
+
else {
|
|
8613
|
+
// No widgets — set on chatData.response (will be the only rendered text)
|
|
8614
|
+
messageToStoreRef.current.chatData.response = abruptCloseMessage;
|
|
8615
|
+
}
|
|
7793
8616
|
setContent(abruptCloseMessage);
|
|
7794
8617
|
// Stop thinking if in progress
|
|
7795
8618
|
if (isThinking) {
|
|
@@ -13331,6 +14154,7 @@ const SmartBot = (props) => {
|
|
|
13331
14154
|
// Dispatch hierarchy key-value pairs to store
|
|
13332
14155
|
dispatch(smartBotActions.setHierarchyKeyValue(hierarchyKeyValuePairs));
|
|
13333
14156
|
setFilterOptions(combinedOptions);
|
|
14157
|
+
dispatch(smartBotActions.setChatbotFilterOptions(combinedOptions));
|
|
13334
14158
|
// Fetch saved filter sets for the plus-button menu
|
|
13335
14159
|
fetchSavedFilterSets();
|
|
13336
14160
|
}
|