diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c58664d..704bf141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.6.0] - 2017-07-07 +### Added +- Added the ability to pass dynamic configuration from parent page to the + bot loader via an event +- Added response cards object display to sample parent page + +### Changed +- Bot loader script now uses its own credential variable instead of setting + it into the global AWS object +- Bumped AWS SDK version in bot loader +- Added functionality to remove event handlers in bot loader for events that + only fire once + +### Fixed +- Typos, invalid links and display issues in README files + +## [0.5.2] - 2017-07-05 +### Fixed +- Credential loading issue in parent bot-loader.js + ## [0.5.1] - 2017-06-06 ### Changed - Copyrights and Amazon software license - + ## [0.5.0] - 2017-06-05 ### Added - Ability to deploy a sample bot based on the OrderFlowers sample diff --git a/lex-web-ui/README.md b/lex-web-ui/README.md index 5140d5fa..ef203a54 100644 --- a/lex-web-ui/README.md +++ b/lex-web-ui/README.md @@ -124,7 +124,7 @@ environment. The files follow this directory structure: Here's an example of the `config.dev.json` file: -```json +``` { "cognito": { "poolId": "us-east-1:deadbeef-cac0-babe-abcd-abcdef01234", @@ -168,7 +168,7 @@ played back. The chatbot UI provides options to control the playback. For example, you can allow to interrupt the playback of long responses and fine tune the various values associated with interruptions: -```json +``` ... lex: { diff --git a/lex-web-ui/package.json b/lex-web-ui/package.json index 50d45f51..13c84596 100755 --- a/lex-web-ui/package.json +++ b/lex-web-ui/package.json @@ -1,6 +1,6 @@ { "name": "lex-web-ui", - "version": "0.5.1", + "version": "0.6.0", "description": "Lex ChatBot Web Interface", "author": "AWS", "license": "Amazon Software License", diff --git a/lex-web-ui/static/iframe/README.md b/lex-web-ui/static/iframe/README.md index b82a9ea1..82c9abbc 100644 --- a/lex-web-ui/static/iframe/README.md +++ b/lex-web-ui/static/iframe/README.md @@ -1,13 +1,13 @@ -###Overview +### Overview The chatbot UI can be embedded in an existing web site by loading it as an iframe. This includes embedding it in a cross-origin setup where the chatbot is served from a server, S3 bucket or CloudFront distribution in a domain that is different from the hosting web site. If you want to know more about the chatbot UI component, please refer to -its [README](https://github.com/awslabs/aws-lex-web-ui/lex-web-ui) file. +its [README](https://github.com/awslabs/aws-lex-web-ui/blob/master/lex-web-ui/README.md) file. -###Adding the ChatBot UI to your Website +### Adding the ChatBot UI to your Website This project provides a sample JavaScript loader [bot-loader.js](./bot-loader.js") and CSS file [bot.css](./bot.css) that can be used to add the chatbot to an existing web site using a @@ -22,12 +22,12 @@ tags to your web page: ``` -###Passing Data Between Parent and ChatBot UI -The chatbot iframe supports passing data to and from the hosting site. This -is done using the JavaScript +### Passing Data Between Parent Page and ChatBot UI +The chatbot iframe supports passing data to and from the hosting parent +page. This is done using the JavaScript [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) -call. This enables use cases such as passing credentials from -the parent hosting site to the chat iframe or passing events (e.g. +call. This mechanism enables use cases such as passing credentials +from the parent hosting site to the chat iframe or passing events (e.g. windows resize) from the chat window to the parent window. The chatbot iframe sends Lex bot state update events to the parent @@ -36,18 +36,32 @@ of the bot. The bot-loader.js script relays these messages back to the parent by emitting the `updatelexstate` events. The event object will contain the Lex state variables in the `details.state` field. -###Configuration -####Parent Configuration -The parent page configuration is held in a JSON config file: -[config.json](./config.json). This file is loaded by -the bot-loader.js script. Here's an example of the file format: -```json +### Configuration +#### Parent Configuration File +The [bot-loader.js](./bot-loader.js) script loads its initial +configuration from the JSON config file: [config.json](./config.json). +This file is meant to be used as the build-time configuration of the +bot-loader.js script. It serves as the base config so the root level +keys in the JSON object should not be removed. + +NOTE: The values in this file may be overwritten by environmental +variables in the build process. + +Here's an example of the file format: +``` { + // iframe origin - see: Cross Origin Configuration section below "iframeOrigin": "http://localhost:8080", + + // time to wait for the config event in ms + "configEventTimeOutInMs": 10000, + + // used to initialize the AWS SDK and Cognito "aws": { "cognitoPoolId": "us-east-1:deadbeef-cac0-babe-abcd-abcdef01234", "region": "us-east-1" } + // chatbot UI configuration passed from parent - see: ChatBot UI Configuration section below "iframeConfig": { ... "lex": { @@ -59,35 +73,67 @@ the bot-loader.js script. Here's an example of the file format: } ``` -####ChatBot UI Configuration -The chatbot UI has a local build-time config (see: -`src/config/config.prod.json`). You can also pass or override this -configuration from the parent site via two mechanisms: - -- **ChatBot UI Configuration from File.** The parent -[config.json](./config.json) file contains the `iframeConfig` field -which is passed to the chatbot UI. This configuration is dynamically -sent to the chatbot UI as a response of the the `onInitIframeConfig` -event. The values delivered via this mechanism override the chatbot -ui local config files and URL config parameter. -- **ChatBot Configuration form URL Parameter.** The chatbot UI -configuration can be initialized using the `config` URL parameter. Your -application can dynamically add the parameter to the URL This is supported -both in iframe and stand-alone mode of the chatbot UI. This config URL -parameter should follow the same JSON structure of the `configDefault` -object in the `src/config/index.js` file. This parameter should be a JSON -serialized and URL encoded string. Values from this parameter override -the ones from the chatbot ui local config files. For example to change -the initialText config field, you can use a URL like this: -`https://mybucket.s3.amazonaws.com/index.html#/?config=%7B%22lex%22%3A%7B%22initialText%22%3A%22Ask%20me%20a%20question%22%7D%7D` - -####Cross Origin Configuration +#### Parent Event Configuration +The parent page can also set the bot loader configuration via an +event. The bot-loader.js script emits the `receivelexconfig` event which +signals to the parent that it is ready to receive a configuration +object. At which point, the bot-loader.js script will wait a 10 +seconds timeout (by default) to receive a event named `loadlexconfig`. +The timeout is controlled by the `configEventTimeOutInMs` field in +the config JSON file. The event object contains the config in the +`detail.config` field. + +The configuration from the JSON file is merged with the value for this +event. The values received via this event take precedence over the +JSON file. + +For example, to pass the browser +[user agent](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorID/userAgent), +you can add code to your site along the lines of: +```javascript +document.addEventListener('receivelexconfig', onReceiveLexConfig, false); + +function onReceiveLexConfig() { + document.removeEventListener('receivelexconfig', onReceiveLexConfig, false); + var config = { + iframeConfig: { + lex: { + sessionAttributes: { + userAgent: navigator.userAgent, + }, + }, + }, + }; + + var event = new CustomEvent('loadlexconfig', { detail: { config: config } }); + document.dispatchEvent(event); +} +``` + +#### ChatBot UI Configuration +The chatbot UI has its own configuration (see the +[README](https://github.com/awslabs/aws-lex-web-ui/blob/master/lex-web-ui/README.md#configuration-and-customization) +for details. You can also pass or override the chatbot UI configuration +from the parent site via the following mechanisms: + +1. **Config Object.** The parent configuration config object (either from +the [config.json](./config.json) file or passed via the `loadlexconfig` +event) contains the `iframeConfig` field which is passed to the chatbot +UI. This configuration is dynamically sent to the chatbot UI as a +response of the the `onInitIframeConfig` event. The values delivered +via this mechanism override the chatbot UI local config files and URL +config parameter. +2. **URL Parameter.** The chatbot UI configuration can be initialized using +the `config` URL parameter. For details, see the +[URL Parameter](https://github.com/awslabs/aws-lex-web-ui/blob/master/lex-web-ui/README.md#url-parameter) +section. NOTE: the bot-loader.js script does not use URL parameters. + +#### Cross Origin Configuration If the chatbot UI is hosted on a different [Origin](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) from the parent window, you need to configure the `iframeOrigin` field -in the parent config.json file to point to the origin of the iframe. This +in the parent `config.json` file to point to the origin of the iframe. This origin configuration is used to control which sites can communicate with the iframe. Conversely, you would need to configure the `ui.parentOrigin` field in the iframe config. The origin configuration of this sample page was done at build time by the CloudFormation stack that created it. - diff --git a/lex-web-ui/static/iframe/bot-loader.js b/lex-web-ui/static/iframe/bot-loader.js index dae89ded..13228bb4 100644 --- a/lex-web-ui/static/iframe/bot-loader.js +++ b/lex-web-ui/static/iframe/bot-loader.js @@ -29,7 +29,7 @@ // AWS SDK script dynamically added to the DOM // https://github.com/aws/aws-sdk-js - sdkLink: 'https://sdk.amazonaws.com/js/aws-sdk-2.60.0.min.js', + sdkLink: 'https://sdk.amazonaws.com/js/aws-sdk-2.82.0.min.js', }; /* @@ -53,6 +53,7 @@ var iframe; var container; var messageHandler = {}; + var credentials; if (isSupported()) { // initialize iframe once the DOM is loaded @@ -78,7 +79,10 @@ } function main() { - loadConfig(configUrl) + loadConfigFromJsonFile(configUrl) + .then(function loadConfigFromEventPromise(conf) { + return loadConfigFromEvent(conf); + }) .then(function assignConfig(conf) { config = conf; return Promise.resolve(); @@ -124,8 +128,8 @@ /** * Loads the bot config from a JSON file URL */ - function loadConfig(url) { - return new Promise(function loadConfigPromise(resolve, reject) { + function loadConfigFromJsonFile(url) { + return new Promise(function loadConfigFromJsonFilePromise(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = 'json'; @@ -147,6 +151,75 @@ }); }; + /** + * Loads dynamic bot config from an event + * Merges it with the config passed as parameter + */ + function loadConfigFromEvent(conf) { + return new Promise(function waitForConfigEvent(resolve, reject) { + var timeoutInMs = conf.configEventTimeOutInMs || 10000; + + var timeoutId = setTimeout(onConfigEventTimeout, timeoutInMs); + document.addEventListener('loadlexconfig', onConfigEventLoaded, false); + + var intervalId = setInterval(emitReceiveEvent, 500); + // signal that we are ready to receive the dynamic config + function emitReceiveEvent() { + var event = new Event('receivelexconfig'); + document.dispatchEvent(event); + }; + + function onConfigEventLoaded(evt) { + clearTimeout(timeoutId); + clearInterval(intervalId); + document.removeEventListener('loadlexconfig', onConfigEventLoaded, false); + + if (evt && ('detail' in evt) && evt.detail && ('config' in evt.detail)) { + var evtConfig = evt.detail.config; + var mergedConfig = mergeConfig(conf, evtConfig); + return resolve(mergedConfig); + } else { + return reject('malformed config event: ' + JSON.stringify(evt)); + } + }; + + function onConfigEventTimeout() { + clearInterval(intervalId); + document.removeEventListener('loadlexconfig', onConfigEventLoaded, false); + return reject('config event timed out'); + }; + }); + + /** + * Merges config objects. The initial set of keys to merge are driven by + * the baseConfig. The srcConfig values override the baseConfig ones. + */ + function mergeConfig(baseConfig, srcConfig) { + // use the baseConfig first level keys as the base for merging + return Object.keys(baseConfig) + .map(function (key) { + var mergedConfig = {}; + var value = baseConfig[key]; + if (key in srcConfig) { + value = (typeof baseConfig[key] === 'object') ? + // recursively merge sub-objects in both directions + Object.assign( + mergeConfig(srcConfig[key], baseConfig[key]), + mergeConfig(baseConfig[key], srcConfig[key]), + ) : + srcConfig[key]; + } + mergedConfig[key] = value; + return mergedConfig; + }) + .reduce(function (merged, configItem) { + return Object.assign({}, merged, configItem); + }, + {} + ); + }; + } + /** * Adds a div container to document body which will wrap the chat bot iframe */ @@ -202,12 +275,12 @@ return Promise.reject('unable to find AWS object'); } - AWS.config.region = config.aws.region; - AWS.config.credentials = new AWS.CognitoIdentityCredentials({ - IdentityPoolId: config.aws.cognitoPoolId, - }); + credentials = new AWS.CognitoIdentityCredentials( + { IdentityPoolId: config.aws.cognitoPoolId }, + { region: config.aws.region }, + ); - return Promise.resolve(); + return credentials.getPromise() } /** @@ -220,21 +293,21 @@ console.log('[INFO] found existing identity ID: ', identityId); } - if (!('getPromise' in AWS.config.credentials)) { + if (!('getPromise' in credentials)) { console.error('getPromise not found in credentials'); return Promise.reject('getPromise not found in credentials'); } - return AWS.config.credentials.getPromise() + return credentials.getPromise() .then(function storeIdentityId() { console.log('[INFO] storing identity ID:', - AWS.config.credentials.identityId + credentials.identityId ); - localStorage.setItem('cognitoid', AWS.config.credentials.identityId); + localStorage.setItem('cognitoid', credentials.identityId); identityId = localStorage.getItem('cognitoid'); }) .then(function getCredentialsPromise() { - return Promise.resolve(AWS.config.credentials); + return Promise.resolve(credentials); }); } @@ -261,6 +334,7 @@ function onIframeLoaded(evt) { clearTimeout(timeoutId); + iframeElement.removeEventListener('load', onIframeLoaded, false); toggleShowUi(); return resolve(iframeElement); }; diff --git a/lex-web-ui/static/iframe/config.json b/lex-web-ui/static/iframe/config.json index 074f07df..41d16f25 100644 --- a/lex-web-ui/static/iframe/config.json +++ b/lex-web-ui/static/iframe/config.json @@ -1,5 +1,6 @@ { "iframeOrigin": "http://localhost:8080", + "configEventTimeOutInMs": 10000, "aws": { "cognitoPoolId": "", "region": "us-east-1" diff --git a/lex-web-ui/static/iframe/index.html b/lex-web-ui/static/iframe/index.html index bf352211..ec416666 100644 --- a/lex-web-ui/static/iframe/index.html +++ b/lex-web-ui/static/iframe/index.html @@ -68,6 +68,19 @@

Lex Chatbot UI Sample Parent Page

+
+
+
+
Lex Response Card
+
+
+

+              
+
+
+
+
+
@@ -76,19 +89,11 @@

Lex Chatbot UI Sample Parent Page

This test application is setup by the CloudFormation template in the aws-lex-web-ui - project. The CloudFormation stack should create the associated config and - resources (e.g. Cognito Identity Pool). + project.

-

- Before using this sample application, you need a working Lex bot. - You can create one by following create the instructions here: - - Create an Amazon Lex Bot (Console). -

-

- Expand the - ChatBot UI Embedding Instructions section below for details. + Expand the ChatBot UI Embedding Instructions + section below for details about integrating it into an existing site.

@@ -137,18 +142,42 @@

console.error('failed to load README.md'); }); - // add bot update event handler - $(document).on('updatelexstate', function OnUpdateLexState(evt) { + // Event handler called when the chatbot ui is ready to receive the + // dynamic config. + // Send dynamic config/parameter (e.g. username, geolocation) to the + // chatbot ui from here + $(document).one('receivelexconfig', function onReceiveLexConfig() { + // this config object should be a valid aws-lex-webui configuration + // values here will override the ones in the JSON config + var config = { + iframeConfig: { + lex: { + sessionAttributes: { + }, + }, + }, + }; + + // emit bot config event + // jquery can't trigger native events so use vanilla JS + var event = new CustomEvent('loadlexconfig', { detail: { config: config } }); + document.dispatchEvent(event); + }); + + // bot update event handler + $(document).on('updatelexstate', function onUpdateLexState(evt) { var slots = {}; var dialogState = {}; var intentName = ''; var sessionAttributes = {}; + var responseCard = {}; if (evt && ('detail' in evt) && evt.detail && ('state' in evt.detail)) { slots = evt.detail.state.slots; dialogState = evt.detail.state.dialogState; intentName = evt.detail.state.intentName || intentName; sessionAttributes = evt.detail.state.sessionAttributes || sessionAttributes; + responseCard = evt.detail.state.responseCard || responseCard; } if (!slots || !dialogState) { console.warn('updatelexstate event is missing slot or dialogState field'); @@ -157,6 +186,7 @@

$('#dialog-state').text(dialogState); $('#intent-name').text(intentName); $('#session-attributes').text(JSON.stringify(sessionAttributes, null, 2)); + $('#response-card').text(JSON.stringify(responseCard, null, 2)); var $slotsContainerReplacement = $('
', { id: 'slots' }); Object.keys(slots).forEach(function updateOrder(slotName, index) {