@ibm-watson/assistant-web-chat-react
Version:
A provider to inject web chat into your React application.
506 lines (486 loc) • 26.8 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var React = require('react');
var ReactDOM = require('react-dom');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
var ReactDOM__default = /*#__PURE__*/_interopDefaultLegacy(ReactDOM);
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
function __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
const semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
const validateAndParse = (version) => {
if (typeof version !== 'string') {
throw new TypeError('Invalid argument expected string');
}
const match = version.match(semver);
if (!match) {
throw new Error(`Invalid argument not valid semver ('${version}' received)`);
}
match.shift();
return match;
};
const isWildcard = (s) => s === '*' || s === 'x' || s === 'X';
const tryParse = (v) => {
const n = parseInt(v, 10);
return isNaN(n) ? v : n;
};
const forceType = (a, b) => typeof a !== typeof b ? [String(a), String(b)] : [a, b];
const compareStrings = (a, b) => {
if (isWildcard(a) || isWildcard(b))
return 0;
const [ap, bp] = forceType(tryParse(a), tryParse(b));
if (ap > bp)
return 1;
if (ap < bp)
return -1;
return 0;
};
const compareSegments = (a, b) => {
for (let i = 0; i < Math.max(a.length, b.length); i++) {
const r = compareStrings(a[i] || '0', b[i] || '0');
if (r !== 0)
return r;
}
return 0;
};
/**
* Compare [semver](https://semver.org/) version strings to find greater, equal or lesser.
* This library supports the full semver specification, including comparing versions with different number of digits like `1.0.0`, `1.0`, `1`, and pre-release versions like `1.0.0-alpha`.
* @param v1 - First version to compare
* @param v2 - Second version to compare
* @returns Numeric value compatible with the [Array.sort(fn) interface](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Parameters).
*/
const compareVersions = (v1, v2) => {
// validate input and split into segments
const n1 = validateAndParse(v1);
const n2 = validateAndParse(v2);
// pop off the patch
const p1 = n1.pop();
const p2 = n2.pop();
// validate numbers
const r = compareSegments(n1, n2);
if (r !== 0)
return r;
// validate pre-release
if (p1 && p2) {
return compareSegments(p1.split('.'), p2.split('.'));
}
else if (p1 || p2) {
return p1 ? -1 : 1;
}
return 0;
};
/**
* (C) Copyright IBM Corp. 2022, 2024.
*
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
*/
/**
* This is a utility component that is used to manage all the user defined responses that are rendered by web chat.
* When a user defined response message is received by web chat, it will fire a "userDefinedResponse" event that
* provides an HTML element to which your application can attach user defined content. React portals are a mechanism
* that allows you to render a component in your React application but attach that component to the HTML element
* that was provided by web chat.
*
* This component will render a portal for each user defined response. The contents of that portal will be
* determined by calling the provided "renderResponse" render prop.
*/
function UserDefinedResponsePortalsContainer(_a) {
var webChatInstance = _a.webChatInstance, renderResponse = _a.renderResponse, userDefinedResponseEvents = _a.userDefinedResponseEvents;
// All we need to do to enable the React portals is to render each portal somewhere in your application (it
// doesn't really matter where).
return (React__default["default"].createElement(React__default["default"].Fragment, null, userDefinedResponseEvents.map(function mapEvent(event, index) {
return (
// eslint-disable-next-line react/no-array-index-key
React__default["default"].createElement(UserDefinedResponseComponentPortal, { key: index, hostElement: event.data.element }, renderResponse(event, webChatInstance)));
})));
}
/**
* This is the component that will attach a React portal to the given host element. The host element is the element
* provided by web chat where your user defined response will be displayed in the DOM. This portal will attach any React
* children passed to it under this component so you can render the response using your own React application. Those
* children will be rendered under the given element where it lives in the DOM.
*/
function UserDefinedResponseComponentPortal(_a) {
var hostElement = _a.hostElement, children = _a.children;
return ReactDOM__default["default"].createPortal(children, hostElement);
}
var UserDefinedResponsePortalsContainerExport = React__default["default"].memo(UserDefinedResponsePortalsContainer);
/**
* (C) Copyright IBM Corp. 2022, 2024.
*
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
*/
// The default host URL where the production version of web chat is hosted.
var DEFAULT_BASE_URL = 'https://web-chat.global.assistant.watson.appdomain.cloud';
// Indicate if debugging is enabled.
var debug = false;
/**
* This is a component wrapper for web chat. This can be rendered anywhere in your application but you should make
* sure it doesn't get unmounted during in the middle of your App's life or it will lose any user defined responses
* that were previously received.
*
* Note that this container will override any config.onLoad property you have set. If you need access to the web
* chat instance or need to perform additional customizations of web chat when it loads, use the onBeforeRender
* callback prop to this component.
*/
function WebChatContainer(_a) {
var onBeforeRender = _a.onBeforeRender, onAfterRender = _a.onAfterRender, renderUserDefinedResponse = _a.renderUserDefinedResponse, config = _a.config, instanceRef = _a.instanceRef, hostURL = _a.hostURL;
// A state value that contains the current instance of web chat.
var _b = React.useState(), instance = _b[0], setInstance = _b[1];
// This state will be used to record all the user defined response events that are fired from the widget. These
// events contain the HTML elements that we will attach our portals to as well as the messages that we wish to
// render in the message.
var _c = React.useState([]), userDefinedResponseEvents = _c[0], setUserDefinedResponseEvents = _c[1];
// The most recent web chat that was load by this component.
var managedWebChatRef = React.useRef();
// The previous web chat config.
var previousConfigRef = React.useRef();
React.useEffect(function () {
var previousConfig = previousConfigRef.current;
previousConfigRef.current = config;
if (previousConfig !== config) {
// Each time the web chat config settings change (or this component is mounted), we need to destroy any previous
// web chat and create a new web chat.
destroyWebChat(managedWebChatRef.current, setInstance, instanceRef);
// We'll use this managed object to keep track of the web chat instance we are creating for this effect.
var managedWebChat_1 = {
instance: null,
shouldDestroy: false,
webChatConfig: config,
};
managedWebChatRef.current = managedWebChat_1;
logger(managedWebChat_1.webChatConfig, "Creating a new web chat due to configuration change.");
// Kick off the creation of a new web chat. This is multistep, asynchronous process.
loadWebChat(managedWebChat_1, hostURL, setInstance, instanceRef, onBeforeRender, onAfterRender, setUserDefinedResponseEvents).catch(function (error) {
logger(managedWebChat_1.webChatConfig, 'An error occurred loading web chat', error);
destroyWebChat(managedWebChat_1, setInstance, instanceRef);
});
return function () {
logger(managedWebChat_1.webChatConfig, "Destroying web chat due to component unmounting.");
destroyWebChat(managedWebChat_1, setInstance, instanceRef);
previousConfigRef.current = null;
};
}
return undefined;
}, [config, hostURL]);
if (renderUserDefinedResponse && instance) {
return (React__default["default"].createElement(UserDefinedResponsePortalsContainerExport, { webChatInstance: instance, renderResponse: renderUserDefinedResponse, userDefinedResponseEvents: userDefinedResponseEvents }));
}
return null;
}
/**
* Loads a new instance of web chat.
*/
function loadWebChat(managedWebChat, hostURL, setInstance, instanceRef, onBeforeRender, onAfterRender, setUserDefinedResponseEvents) {
return __awaiter(this, void 0, void 0, function () {
var webChatConfig, message, configWithoutOnLoad, instance;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
webChatConfig = managedWebChat.webChatConfig;
// The first step is to make sure the javascript for web chat is loaded.
return [4 /*yield*/, ensureWebChatScript(webChatConfig, hostURL)];
case 1:
// The first step is to make sure the javascript for web chat is loaded.
_a.sent();
if (managedWebChat.shouldDestroy) {
logger(webChatConfig, "Destroying web chat before an instance is created.");
destroyWebChat(managedWebChat, setInstance, instanceRef);
return [2 /*return*/];
}
// Now create an instance of web chat.
if (webChatConfig.onLoad) {
message = 'Do not use onLoad in the web chat config. Use the WebChatContainer onBeforeRender or onAfterRender prop instead.';
logger(webChatConfig, message);
}
logger(webChatConfig, "Creating web chat instance.");
configWithoutOnLoad = __assign(__assign({}, webChatConfig), { onLoad: null });
return [4 /*yield*/, window.loadWatsonAssistantChat(configWithoutOnLoad)];
case 2:
instance = _a.sent();
// Even if it doesn't end up being used, we need to add a userDefinedResponse listener now so we can ensure that
// we catch any events that may be fired during the render call (like the welcome message).
addUserDefinedResponseHandler(instance, setUserDefinedResponseEvents);
// Once the instance is created, call the onBeforeRender and then render and then onAfterRender.
return [4 /*yield*/, (onBeforeRender === null || onBeforeRender === void 0 ? void 0 : onBeforeRender(instance))];
case 3:
// Once the instance is created, call the onBeforeRender and then render and then onAfterRender.
_a.sent();
logger(webChatConfig, "Calling render.");
return [4 /*yield*/, instance.render()];
case 4:
_a.sent();
return [4 /*yield*/, (onAfterRender === null || onAfterRender === void 0 ? void 0 : onAfterRender(instance))];
case 5:
_a.sent();
// Update the state of the parent component with the instance.
setInstance(instance);
managedWebChat.instance = instance;
if (instanceRef) {
instanceRef.current = instance;
}
if (managedWebChat.shouldDestroy) {
logger(webChatConfig, "Destroying web chat after an instance is created but before calling onLoad.");
destroyWebChat(managedWebChat, setInstance, instanceRef);
}
return [2 /*return*/];
}
});
});
}
/**
* Adds a "userDefinedResponse" event listener to the given web chat instance that will use the given set function
* to add new events to the list.
*/
function addUserDefinedResponseHandler(webChatInstance, setUserDefinedResponseEvents) {
// This handler will fire each time a user defined response occurs and we will update our state by appending the
// event to the end of our events list. We have to make sure to create a new array in order to trigger a re-render.
function userDefinedResponseHandler(event) {
setUserDefinedResponseEvents(function (eventsArray) { return eventsArray.concat(event); });
}
// Also make sure to clear the list if a restart occurs.
function restartHandler() {
setUserDefinedResponseEvents([]);
}
// In web chat 8.2.0, the "customResponse" event was renamed to "userDefinedResponse".
var webChatVersion = webChatInstance.getWidgetVersion();
var eventName = compareVersions(webChatVersion, '8.2.0') >= 0 ? 'userDefinedResponse' : 'customResponse';
webChatInstance.on({ type: eventName, handler: userDefinedResponseHandler });
webChatInstance.on({ type: 'restartConversation', handler: restartHandler });
}
/**
* Destroys an instance of web chat and marks it destroyed.
*/
function destroyWebChat(managedWebChat, setInstance, instanceRef) {
if (managedWebChat) {
if (managedWebChat.instance) {
logger(managedWebChat.webChatConfig, "Destroying web chat instance.");
managedWebChat.instance.destroy();
}
managedWebChat.shouldDestroy = true;
managedWebChat.instance = null;
}
setInstance(null);
if (instanceRef) {
instanceRef.current = null;
}
}
/**
* A public function that can be used to turn logging off or on.
*/
function setEnableDebug(enableDebug) {
debug = enableDebug;
}
/**
* A convenience function for logging to the console.
*/
function logger(webChatConfig) {
var args = [];
for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
if (debug) {
var namespaceLabel = (webChatConfig === null || webChatConfig === void 0 ? void 0 : webChatConfig.namespace) ? ": Namespace ".concat(webChatConfig.namespace) : '';
// eslint-disable-next-line no-console
console.log.apply(console, __spreadArray(["[IBM watsonx Assistant WebChatContainer".concat(namespaceLabel, "]")], args, false));
}
}
/**
* Removes any trailing slash from the given string.
*/
function removeTrailingSlash(value) {
return value.replace(/\/$/, '');
}
/**
* Ensures that the javascript for web chat has been loaded.
*/
function ensureWebChatScript(webChatConfig, hostURL) {
return __awaiter(this, void 0, void 0, function () {
var useURL, scriptURL, loadedWebChatURL, message;
return __generator(this, function (_a) {
useURL = DEFAULT_BASE_URL;
if (hostURL) {
useURL = removeTrailingSlash(hostURL);
}
else if (webChatConfig.cloudPrivateHostURL) {
useURL = "".concat(removeTrailingSlash(webChatConfig.cloudPrivateHostURL), "/static/webchat");
}
scriptURL = "".concat(useURL, "/versions/").concat(webChatConfig.clientVersion || 'latest', "/WatsonAssistantChatEntry.js");
loadedWebChatURL = window.wacWebChatContainerScriptURL;
if (loadedWebChatURL && loadedWebChatURL !== scriptURL) {
message = "Web chat has already been loaded using a different URL (".concat(loadedWebChatURL, "). This component does not support loading web chat using multiple URLs including different versions of web chat. The current code attempted to load from ").concat(scriptURL, ".");
logger(null, message);
}
// Check to see if we already have a Promise for loading this script. We're using a window property to cover the
// case where multiple library instances are being used that can't necessarily share module state.
if (!window.wacWebChatContainerScriptPromise) {
logger(null, "Loading the web chat javascript from ".concat(scriptURL, "."));
window.wacWebChatContainerScriptPromise = new Promise(function (resolve, reject) {
var scriptElement = document.createElement('script');
scriptElement.onload = function () { return resolve(); };
scriptElement.onerror = function () { return reject(); };
scriptElement.src = scriptURL;
document.head.appendChild(scriptElement);
window.wacWebChatContainerScriptURL = scriptURL;
});
}
return [2 /*return*/, window.wacWebChatContainerScriptPromise];
});
});
}
/**
* (C) Copyright IBM Corp. 2022.
*
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
*/
var CUSTOM_ELEMENT_STYLES = "\n#WACContainer.WACContainer .HideWebChat { display: none }\n.CustomElementHideWebChat { width: 0; height: 0 }\n";
/**
* This component can be used if you want to render web chat inside a custom element. It will perform two functions:
*
* 1. It will create the custom element as part of the React application.
* 2. It will attach web chat to the custom element and use the WebChatContainer component to manage the life cycle
* of the web chat instance.
*/
function WebChatCustomElement(props) {
var _this = this;
var className = props.className, id = props.id, onViewChange = props.onViewChange, config = props.config, onBeforeRender = props.onBeforeRender, containerProps = __rest(props, ["className", "id", "onViewChange", "config", "onBeforeRender"]);
var _a = React.useState(), customElement = _a[0], setCustomElement = _a[1];
// Make sure to memoize the config object. If we pass a new object to WebChatContainer (even if all the properties
// inside of it are the same), the container will just continually destroy and recreate the web chat instance
// because it thinks the config keeps changing.
var useConfig = React.useMemo(function () {
return __assign(__assign({}, config), { element: customElement });
}, [config, customElement]);
var onBeforeRenderOverride = React.useCallback(function (instance) { return __awaiter(_this, void 0, void 0, function () {
/**
* A default handler for the "view:change" event. This will be used to show or hide the web chat main window
* using a simple classname.
*/
function defaultViewChangeHandler(event, instance) {
if (event.newViewState.mainWindow) {
customElement.classList.remove('CustomElementHideWebChat');
instance.elements.getMainWindow().removeClassName('HideWebChat');
}
else {
customElement.classList.add('CustomElementHideWebChat');
instance.elements.getMainWindow().addClassName('HideWebChat');
}
}
return __generator(this, function (_a) {
instance.on({ type: 'view:change', handler: onViewChange || defaultViewChangeHandler });
return [2 /*return*/, onBeforeRender === null || onBeforeRender === void 0 ? void 0 : onBeforeRender(instance)];
});
}); }, [onBeforeRender, onViewChange, customElement]);
return (React__default["default"].createElement(React__default["default"].Fragment, null,
React__default["default"].createElement("div", { className: className, id: id, ref: setCustomElement }),
!onViewChange && React__default["default"].createElement("style", null, CUSTOM_ELEMENT_STYLES),
customElement && (React__default["default"].createElement(WebChatContainer, __assign({ config: useConfig, onBeforeRender: onBeforeRenderOverride }, containerProps)))));
}
exports.WebChatContainer = WebChatContainer;
exports.WebChatCustomElement = WebChatCustomElement;
exports.setEnableDebug = setEnableDebug;