diff --git a/Node/cards-AdaptiveCards/.env b/Node/cards-AdaptiveCards/.env new file mode 100644 index 0000000000..d5f29e6150 --- /dev/null +++ b/Node/cards-AdaptiveCards/.env @@ -0,0 +1,4 @@ +# Bot Framework Credentials + +MICROSOFT_APP_ID= +MICROSOFT_APP_PASSWORD= diff --git a/Node/cards-AdaptiveCards/README.md b/Node/cards-AdaptiveCards/README.md new file mode 100644 index 0000000000..b78a38d290 --- /dev/null +++ b/Node/cards-AdaptiveCards/README.md @@ -0,0 +1,386 @@ +# Adaptive Cards Bot Sample + +A sample bot using [Adaptive Cards](http://adaptivecards.io/) and how to handle user interaction with them. + +[![Deploy to Azure][Deploy Button]][Deploy Node/AdaptiveCards] + +[Deploy Button]: https://azuredeploy.net/deploybutton.png +[Deploy Node/AdaptiveCards]: https://azuredeploy.net + +### Prerequisites + +The minimum prerequisites to run this sample are: +* Latest Node.js with NPM. Download it from [here](https://nodejs.org/en/download/). +* The Bot Framework Emulator. To install the Bot Framework Emulator, download it from [here](https://emulator.botframework.com/). Please refer to [this documentation article](https://github.com/microsoft/botframework-emulator/wiki/Getting-Started) to know more about the Bot Framework Emulator. +* **[Recommended]** Visual Studio Code for IntelliSense and debugging, download it from [here](https://code.visualstudio.com/) for free. + +### Code Highlights + +[Adaptive Cards](http://adaptivecards.io/) are an open card exchange format that enables developers to exchange UI content in a common and consistent way. The Bot Framework has the ability to use this type of cards and provide a richer interaction experience. + +The Adaptive Card can contain any combination of text, speech, images, buttons, and input fields. Adaptive Cards are created using the JSON format specified in the Adaptive Cards schema, which gives you full control over card content and format. + +The aesthetics of the card are adapted to the channel's look and feel, making it feel native to the app and familiar to the user. You can use the [Adaptive Cards' Visualizer](http://adaptivecards.io/visualizer) to see how your card renders on different channels. + +> Note: At the time of writing this sample, the Adaptive Cards support on the different channels is limited. This sample works properly on the Emulator and WebChat channel. See [more information](https://github.com/Microsoft/AdaptiveCards/issues/367) about channel support. + +See how the sample composes a [welcome card](app.js#L30-L156) along with search options: + +````JavaScript +var card = { + 'contentType': 'application/vnd.microsoft.card.adaptive', + 'content': { + '$schema': 'http://adaptivecards.io/schemas/adaptive-card.json', + 'type': 'AdaptiveCard', + 'version': '1.0', + 'body': [ + { + 'type': 'Container', + 'speak': 'Hello!Are you looking for a flight or a hotel?', + 'items': [ + { + 'type': 'ColumnSet', + 'columns': [ + { + 'type': 'Column', + 'size': 'auto', + 'items': [ + { + 'type': 'Image', + 'url': 'https://placeholdit.imgix.net/~text?txtsize=65&txt=Adaptive+Cards&w=300&h=300', + 'size': 'medium', + 'style': 'person' + } + ] + }, + { + 'type': 'Column', + 'size': 'stretch', + 'items': [ + { + 'type': 'TextBlock', + 'text': 'Hello!', + 'weight': 'bolder', + 'isSubtle': true + }, + { + 'type': 'TextBlock', + 'text': 'Are you looking for a flight or a hotel?', + 'wrap': true + } + ] + } + ] + } + ] + } + ], + 'actions': [ /* */ ] + } +}; +```` + +The previous code will generate a card similar to this one: + +![Welcome Card](images/welcome-card.png) + +Adaptive Cards are created using JSON, like the one depicted above, and sent as a message attachment: + +````JavaScript +var msg = new builder.Message(session) + .addAttachment(card); +session.send(msg); +```` + +Adaptive Cards contain many elements that allow to exchange UI content in a common and consistent way. Some of these elements are: + +- **TextBlock** + + The TextBlock element allows for the inclusion of text, with various font sizes, weight and color. + +- **ImageSet** and **Image** + + The ImageSet allows for the inclusion of a collection images like a photo set, and the Image element allows for the inclusion of images. + +- **Input elements** + + Input elements allow you to ask for native UI to build simple forms: + + - **Input.Text** - get text content from the user + - **Input.Date** - get a Date from the user + - **Input.Time** - get a Time from the user + - **Input.Number** - get a Number from the user + - **Input.ChoiceSet** - Give the user a set of choices and have them pick + - **Input.ToggleChoice** - Give the user a single choice between two items and have them pick + +- **Container** + + A Container is a CardElement which contains a list of CardElements that are logically grouped. + +- **ColumnSet** and **Column** + + The columnSet element adds the ability to have a set of Column objects. + +- **FactSet** + + The FactSet element makes it simple to display a series of "facts" (e.g. name/value pairs) in a tabular form. + +Finally, Adaptive Cards support special elements that enable interaction: + +- **Action.OpenUrl** + + When Action.OpenUrl is invoked it will show the given url, either by launching it to an external web browser or showing in-situ with embedded web browser. + +- **Action.Submit** + + Action.Submit gathers up input fields, merges with optional data field and generates event to client asking for data to be submitted. The Bot Framework will send an activity through the messaging medium to the bot. + +- **Action.Http** + + Action.Http represents the properties needed to do an Http request. All input properties are available for use via data binding. Properties can be data bound to the Uri and Body properties, allowing you to send a request to an arbitrary url. + +- **Action.ShowCard** + + Action.ShowCard defines an inline AdaptiveCard which is shown to the user when it is clicked. + +You can visit the [Adaptive Cards Schema Explorer](http://adaptivecards.io/explorer/) for samples and the properties each element supports. + +#### Creating an inline Adaptive Card + +A card may offer the user multiple options to continue. Each option can be offered as a button that, once clicked, expands into a new card within the existing one. This is accomplised using a *ShowCard Action*. +See app.js [Flight's option](app.js#L138-L153) for a simple card and [Hotel's option](app.js#L80-L137) for a complex one. +These are defined within the `actions` element of the main card. See below how the `type` of each action is defined as `Action.ShowCard` and the `card` property contains a new Adaptive Card. + +````JavaScript +var card = { + 'contentType': 'application/vnd.microsoft.card.adaptive', + 'content': { + '$schema': 'http://adaptivecards.io/schemas/adaptive-card.json', + 'type': 'AdaptiveCard', + 'version': '1.0', + 'body': [ /* */ ], + 'actions': [ + // Hotels Search form + { + 'type': 'Action.ShowCard', + 'title': 'Hotels', + 'speak': 'Hotels', + 'card': { + 'type': 'AdaptiveCard', + 'body': [ /* */ ], + 'actions': [ + { + 'type': 'Action.Submit', + 'title': 'Search', + 'speak': 'Search', + 'data': { + 'type': 'hotelSearch' + } + } + ] + } + }, + { + 'type': 'Action.ShowCard', + 'title': 'Flights', + 'speak': 'Flights', + 'card': { + 'type': 'AdaptiveCard', + 'body': [ + { + 'type': 'TextBlock', + 'text': 'Flights is not implemented =(', + 'speak': 'Flights is not implemented', + 'weight': 'bolder' + } + ] + } + } + ] + } +}; +```` + +#### Collecting and handling input from the user + +Adaptive Cards can include input controls for gathering information from the user that is viewing the card. + +At the time of writing this sample, the Adaptive Cards support for input controls is: [Text](http://adaptivecards.io/explorer/#InputText), [Date](http://adaptivecards.io/explorer/#InputDate), [Time](http://adaptivecards.io/explorer/#InputTime), [Number](http://adaptivecards.io/explorer/#InputNumber) and for selecting options there are the [Toggle](http://adaptivecards.io/explorer/#InputToggle) and [ChoiceSet](http://adaptivecards.io/explorer/#InputChoiceSet). + +See app.js hotel's search form for a simple sample: + +````JavaScript +{ + 'type': 'Action.ShowCard', + 'title': 'Hotels', + 'speak': 'Hotels', + 'card': { + 'type': 'AdaptiveCard', + 'body': [ + { + 'type': 'TextBlock', + 'text': 'Welcome to the Hotels finder!', + 'speak': 'Welcome to the Hotels finder!', + 'weight': 'bolder', + 'size': 'large' + }, + { + 'type': 'TextBlock', + 'text': 'Please enter your destination:' + }, + { + 'type': 'Input.Text', + 'id': 'destination', + 'speak': 'Please enter your destination', + 'placeholder': 'Miami, Florida', + 'style': 'text' + }, + { + 'type': 'TextBlock', + 'text': 'When do you want to check in?' + }, + { + 'type': 'Input.Date', + 'id': 'checkin', + 'speak': 'When do you want to check in?' + }, + { + 'type': 'TextBlock', + 'text': 'How many nights do you want to stay?' + }, + { + 'type': 'Input.Number', + 'id': 'nights', + 'min': 1, + 'max': 60, + 'speak': 'How many nights do you want to stay?' + } + ], + 'actions': [ + { + 'type': 'Action.Submit', + 'title': 'Search', + 'speak': 'Search', + 'data': { + 'type': 'hotelSearch' + } + } + ] + } +} +```` + +The above card will generate a card similar to this one: + +![Search Form Card](images/search-form-card.png) + +Submitting the information can be be done in two possible ways: + +- **Http** + + Action.Http represents the properties needed to do an Http request. All input properties are available for use via data binding. Properties can be data bound to the Uri and Body properties, allowing you to send a request to an arbitrary url. This method can be used to call a service hosted elsewhere through HTTP. + +- **Submit** + + Action.Submit gathers up input fields, merges with optional data field and generates event to client asking for data to be submitted. The Bot Framework will send an activity through the messaging medium to the bot. This is the method used in the sample. + +When using the **Submit** method, the Bot Framework will handle the submission and your bot will receive a new message with its `value` field filled with the form data as a JSON object. + +````JavaScript +var bot = new builder.UniversalBot(connector, function (session) { + + if (session.message && session.message.value) { + // A Card's Submit Action obj was received + processSubmitAction(session, session.message.value); + return; + } + + // ... +}); +```` + +![Search Form Submission](images/search-form-submit.png) + +You'll note in the `Action.Submit` action that there is a `data` field with a `{ 'type': 'hotelSearch' }` object. The `type` attribute is used To later identify the originating submit action. When submitting, the Adaptive Card combines the form values to the [Submit Action's `data` property](http://adaptivecards.io/explorer/#ActionSubmit). + +Once received the search form parameters, [validation is triggered](app.js#L182), and once it passes, the [`hotels-search`](hotels-search.js) dialog is called with the search parameters as the dialog's argument: + +````JavaScript +session.beginDialog('hotels-search', value); +```` + +#### Displaying information with ColumnSet + +For displaying the hotel search results, the sample uses `ColumnSet` and `Columns` to format them into rows and columns. See how the [`hotels-search` dialog](hotels-search.js#L24-L43) makes use of these elements to create the layout depicted below: + +````JavaScript +var rows = _.chunk(hotels, 3).map(group => + ({ + 'type': 'ColumnSet', + 'columns': group.map(asHotelItem) + })); + +var card = { + 'contentType': 'application/vnd.microsoft.card.adaptive', + 'content': { + 'type': 'AdaptiveCard', + 'body': [ + { + 'type': 'TextBlock', + 'text': title, + 'size': 'extraLarge', + 'speak': '' + title + '' + } + ].concat(rows) + } +}; + +// Helpers +function asHotelItem(hotel) { + return { + 'type': 'Column', + 'size': '20', + 'items': [ + { + 'type': 'TextBlock', + 'horizontalAlignment': 'center', + 'wrap': false, + 'weight': 'bolder', + 'text': hotel.name, + 'speak': '' + hotel.name + '' + }, + { + 'type': 'Image', + 'size': 'auto', + 'url': hotel.image + } + ], + 'selectAction': { + 'type': 'Action.Submit', + 'data': _.extend({ type: 'hotelSelection' }, hotel) + } + }; +} +```` + +![Search Results Layout](images/search-results-layout.png) + +### Outcome + +You will see the following in the Bot Framework Emulator when opening and running the sample. + +![Sample Outcome Welcome](images/outcome-1.png) + +![Sample Outcome Results](images/outcome-2.png) + +### More Information + +To get more information about how to get started in Bot Builder for Node and Attachments please review the following resources: +* [Bot Builder for Node.js Reference](https://docs.microsoft.com/en-us/bot-framework/nodejs/) +* [Adaptive Cards](http://adaptivecards.io/) +* [Adaptive Cards Visualizer](http://adaptivecards.io/visualizer/) +* [Adaptive Cards Schema Explorer](http://adaptivecards.io/explorer/) +* [Send an Adaptive Card](https://docs.microsoft.com/en-us/bot-framework/nodejs/bot-builder-nodejs-send-rich-cards#send-an-adaptive-card) + +> **Limitations** +> The functionality provided in this sample only works with WebChat and the Emulator. Other channels have limited functionality as described in the following [link](https://github.com/Microsoft/AdaptiveCards/issues/367). \ No newline at end of file diff --git a/Node/cards-AdaptiveCards/app.js b/Node/cards-AdaptiveCards/app.js new file mode 100644 index 0000000000..9559bc142e --- /dev/null +++ b/Node/cards-AdaptiveCards/app.js @@ -0,0 +1,275 @@ +// This loads the environment variables from the .env file +require('dotenv-extended').load(); + +var util = require('util'); +var builder = require('botbuilder'); +var restify = require('restify'); + +// Setup Restify Server +var server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, function () { + console.log('%s listening to %s', server.name, server.url); +}); + +// Create chat bot and listen to messages +var connector = new builder.ChatConnector({ + appId: process.env.MICROSOFT_APP_ID, + appPassword: process.env.MICROSOFT_APP_PASSWORD +}); +server.post('/api/messages', connector.listen()); + +var bot = new builder.UniversalBot(connector, function (session) { + + if (session.message && session.message.value) { + // A Card's Submit Action obj was received + processSubmitAction(session, session.message.value); + return; + } + + // Display Welcome card with Hotels and Flights search options + var card = { + 'contentType': 'application/vnd.microsoft.card.adaptive', + 'content': { + '$schema': 'http://adaptivecards.io/schemas/adaptive-card.json', + 'type': 'AdaptiveCard', + 'version': '1.0', + 'body': [ + { + 'type': 'Container', + 'speak': 'Hello!Are you looking for a flight or a hotel?', + 'items': [ + { + 'type': 'ColumnSet', + 'columns': [ + { + 'type': 'Column', + 'size': 'auto', + 'items': [ + { + 'type': 'Image', + 'url': 'https://placeholdit.imgix.net/~text?txtsize=65&txt=Adaptive+Cards&w=300&h=300', + 'size': 'medium', + 'style': 'person' + } + ] + }, + { + 'type': 'Column', + 'size': 'stretch', + 'items': [ + { + 'type': 'TextBlock', + 'text': 'Hello!', + 'weight': 'bolder', + 'isSubtle': true + }, + { + 'type': 'TextBlock', + 'text': 'Are you looking for a flight or a hotel?', + 'wrap': true + } + ] + } + ] + } + ] + } + ], + 'actions': [ + // Hotels Search form + { + 'type': 'Action.ShowCard', + 'title': 'Hotels', + 'speak': 'Hotels', + 'card': { + 'type': 'AdaptiveCard', + 'body': [ + { + 'type': 'TextBlock', + 'text': 'Welcome to the Hotels finder!', + 'speak': 'Welcome to the Hotels finder!', + 'weight': 'bolder', + 'size': 'large' + }, + { + 'type': 'TextBlock', + 'text': 'Please enter your destination:' + }, + { + 'type': 'Input.Text', + 'id': 'destination', + 'speak': 'Please enter your destination', + 'placeholder': 'Miami, Florida', + 'style': 'text' + }, + { + 'type': 'TextBlock', + 'text': 'When do you want to check in?' + }, + { + 'type': 'Input.Date', + 'id': 'checkin', + 'speak': 'When do you want to check in?' + }, + { + 'type': 'TextBlock', + 'text': 'How many nights do you want to stay?' + }, + { + 'type': 'Input.Number', + 'id': 'nights', + 'min': 1, + 'max': 60, + 'speak': 'How many nights do you want to stay?' + } + ], + 'actions': [ + { + 'type': 'Action.Submit', + 'title': 'Search', + 'speak': 'Search', + 'data': { + 'type': 'hotelSearch' + } + } + ] + } + }, + { + 'type': 'Action.ShowCard', + 'title': 'Flights', + 'speak': 'Flights', + 'card': { + 'type': 'AdaptiveCard', + 'body': [ + { + 'type': 'TextBlock', + 'text': 'Flights is not implemented =(', + 'speak': 'Flights is not implemented', + 'weight': 'bolder' + } + ] + } + } + ] + } + }; + + var msg = new builder.Message(session) + .addAttachment(card); + session.send(msg); +}); + +// Search Hotels +bot.dialog('hotels-search', require('./hotels-search')); + +// Help +bot.dialog('support', require('./support')) + .triggerAction({ + matches: [/help/i, /support/i, /problem/i] + }); + +// log any bot errors into the console +bot.on('error', function (e) { + console.log('And error ocurred', e); +}); + +function processSubmitAction(session, value) { + var defaultErrorMessage = 'Please complete all the search parameters'; + switch (value.type) { + case 'hotelSearch': + // Search, validate parameters + if (validateHotelSearch(value)) { + // proceed to search + session.beginDialog('hotels-search', value); + } else { + session.send(defaultErrorMessage); + } + break; + + case 'hotelSelection': + // Hotel selection + sendHotelSelection(session, value); + break; + + default: + // A form data was received, invalid or incomplete since the previous validation did not pass + session.send(defaultErrorMessage); + } +} + +function validateHotelSearch(hotelSearch) { + if (!hotelSearch) { + return false; + } + + // Destination + var hasDestination = typeof hotelSearch.destination === 'string' && hotelSearch.destination.length > 3; + + // Checkin + var checkin = Date.parse(hotelSearch.checkin); + var hasCheckin = !isNaN(checkin); + if (hasCheckin) { + hotelSearch.checkin = new Date(checkin); + } + + // Nights + var nights = parseInt(hotelSearch.nights, 10); + var hasNights = !isNaN(nights); + if (hasNights) { + hotelSearch.nights = nights; + } + + return hasDestination && hasCheckin && hasNights; +} + +function sendHotelSelection(session, hotel) { + var description = util.format('%d stars with %d reviews. From $%d per night.', hotel.rating, hotel.numberOfReviews, hotel.priceStarting); + var card = { + 'contentType': 'application/vnd.microsoft.card.adaptive', + 'content': { + 'type': 'AdaptiveCard', + 'body': [ + { + 'type': 'Container', + 'items': [ + { + 'type': 'TextBlock', + 'text': hotel.name + ' in ' + hotel.location, + 'weight': 'bolder', + 'speak': '' + hotel.name + '' + }, + { + 'type': 'TextBlock', + 'text': description, + 'speak': '' + description + '' + }, + { + 'type': 'Image', + 'size': 'auto', + 'url': hotel.image + }, + { + 'type': 'ImageSet', + 'imageSize': 'medium', + 'separation': 'strong', + 'images': hotel.moreImages.map((img) => ({ + 'type': 'Image', + 'url': img + })) + } + ], + 'selectAction': { + 'type': 'Action.OpenUrl', + 'url': 'https://dev.botframework.com/' + } + } + ] + } + }; + + var msg = new builder.Message(session) + .addAttachment(card); + + session.send(msg); +} \ No newline at end of file diff --git a/Node/cards-AdaptiveCards/azuredeploy.json b/Node/cards-AdaptiveCards/azuredeploy.json new file mode 100644 index 0000000000..b42df36f07 --- /dev/null +++ b/Node/cards-AdaptiveCards/azuredeploy.json @@ -0,0 +1,129 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteName": { + "defaultValue": "BotBuilder-Samples", + "type": "string" + }, + "hostingPlanName": { + "type": "string" + }, + "siteLocation": { + "type": "string" + }, + "sku": { + "type": "string", + "allowedValues": [ + "Free", + "Shared", + "Basic", + "Standard" + ], + "defaultValue": "Free" + }, + "workerSize": { + "type": "string", + "allowedValues": [ + "0", + "1", + "2" + ], + "defaultValue": "0" + }, + "repoUrl": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "Project": { + "type": "string", + "defaultValue": "Node/cards-AdaptiveCards" + }, + "WEBSITE_NODE_DEFAULT_VERSION": { + "type": "string", + "defaultValue": "6.9.5" + }, + "MICROSOFT_APP_ID": { + "type": "string" + }, + "MICROSOFT_APP_PASSWORD": { + "type": "string" + } + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "name": "[parameters('hostingPlanName')]", + "type": "Microsoft.Web/serverFarms", + "location": "[parameters('siteLocation')]", + "properties": { + "name": "[parameters('hostingPlanName')]", + "sku": "[parameters('sku')]", + "workerSize": "[parameters('workerSize')]", + "numberOfWorkers": 1 + } + }, + { + "apiVersion": "2014-06-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Web/Sites", + "location": "[parameters('siteLocation')]", + "dependsOn": [ + "[concat('Microsoft.Web/serverFarms/', parameters('hostingPlanName'))]" + ], + "tags": { + "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "empty" + }, + "properties": { + "name": "[parameters('siteName')]", + "serverFarm": "[parameters('hostingPlanName')]" + }, + "resources": [ + { + "apiVersion": "2014-04-01", + "type": "config", + "name": "web", + "dependsOn": [ + "[concat('Microsoft.Web/Sites/', parameters('siteName'))]" + ], + "properties": { + "appSettings": [ + { + "name": "Project", + "value": "[parameters('Project')]" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "[parameters('WEBSITE_NODE_DEFAULT_VERSION')]" + }, + { + "name": "MICROSOFT_APP_ID", + "value": "[parameters('MICROSOFT_APP_ID')]" + }, + { + "name": "MICROSOFT_APP_PASSWORD", + "value": "[parameters('MICROSOFT_APP_PASSWORD')]" + } + ] + } + }, + { + "apiVersion": "2014-04-01", + "name": "web", + "type": "sourcecontrols", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]", + "[concat('Microsoft.Web/Sites/', parameters('siteName'), '/config/web')]" + ], + "properties": { + "RepoUrl": "[parameters('repoUrl')]", + "branch": "[parameters('branch')]", + "IsManualIntegration": true + } + } + ] + } + ] +} \ No newline at end of file diff --git a/Node/cards-AdaptiveCards/hotels-search.js b/Node/cards-AdaptiveCards/hotels-search.js new file mode 100644 index 0000000000..66a9ed8a07 --- /dev/null +++ b/Node/cards-AdaptiveCards/hotels-search.js @@ -0,0 +1,84 @@ +var util = require('util'); +var _ = require('lodash'); +var builder = require('botbuilder'); +var Store = require('./store'); + +module.exports = function search(session, hotelSearch) { + var destination = hotelSearch.destination; + var checkIn = hotelSearch.checkin; + var checkOut = checkIn.addDays(hotelSearch.nights); + + session.send( + 'Ok. Searching for Hotels in %s from %d/%d to %d/%d...', + destination, + checkIn.getMonth() + 1, checkIn.getDate(), + checkOut.getMonth() + 1, checkOut.getDate()); + + // Async search + Store + .searchHotels(destination, checkIn, checkOut) + .then(function (hotels) { + // Results + var title = util.format('I found in total %d hotels for your dates:', hotels.length); + + var rows = _.chunk(hotels, 3).map(group => + ({ + 'type': 'ColumnSet', + 'columns': group.map(asHotelItem) + })); + + var card = { + 'contentType': 'application/vnd.microsoft.card.adaptive', + 'content': { + 'type': 'AdaptiveCard', + 'body': [ + { + 'type': 'TextBlock', + 'text': title, + 'size': 'extraLarge', + 'speak': '' + title + '' + } + ].concat(rows) + } + }; + + var msg = new builder.Message(session) + .addAttachment(card); + session.send(msg); + }); + + session.endDialog(); +}; + +// Helpers +function asHotelItem(hotel) { + return { + 'type': 'Column', + 'size': '20', + 'items': [ + { + 'type': 'TextBlock', + 'horizontalAlignment': 'center', + 'wrap': false, + 'weight': 'bolder', + 'text': hotel.name, + 'speak': '' + hotel.name + '' + }, + { + 'type': 'Image', + 'size': 'auto', + 'url': hotel.image + } + ], + 'selectAction': { + 'type': 'Action.Submit', + 'data': _.extend({ type: 'hotelSelection' }, hotel) + } + }; +} + +Date.prototype.addDays = function (days) { + var date = new Date(this.valueOf()); + date.setDate(date.getDate() + days); + return date; +}; \ No newline at end of file diff --git a/Node/cards-AdaptiveCards/images/outcome-1.png b/Node/cards-AdaptiveCards/images/outcome-1.png new file mode 100644 index 0000000000..7c310baaae Binary files /dev/null and b/Node/cards-AdaptiveCards/images/outcome-1.png differ diff --git a/Node/cards-AdaptiveCards/images/outcome-2.png b/Node/cards-AdaptiveCards/images/outcome-2.png new file mode 100644 index 0000000000..bcf58f7556 Binary files /dev/null and b/Node/cards-AdaptiveCards/images/outcome-2.png differ diff --git a/Node/cards-AdaptiveCards/images/search-form-card.png b/Node/cards-AdaptiveCards/images/search-form-card.png new file mode 100644 index 0000000000..720b558b7f Binary files /dev/null and b/Node/cards-AdaptiveCards/images/search-form-card.png differ diff --git a/Node/cards-AdaptiveCards/images/search-form-submit.png b/Node/cards-AdaptiveCards/images/search-form-submit.png new file mode 100644 index 0000000000..c3d373f459 Binary files /dev/null and b/Node/cards-AdaptiveCards/images/search-form-submit.png differ diff --git a/Node/cards-AdaptiveCards/images/search-results-layout.png b/Node/cards-AdaptiveCards/images/search-results-layout.png new file mode 100644 index 0000000000..284d228dd7 Binary files /dev/null and b/Node/cards-AdaptiveCards/images/search-results-layout.png differ diff --git a/Node/cards-AdaptiveCards/images/welcome-card.png b/Node/cards-AdaptiveCards/images/welcome-card.png new file mode 100644 index 0000000000..ed52367136 Binary files /dev/null and b/Node/cards-AdaptiveCards/images/welcome-card.png differ diff --git a/Node/cards-AdaptiveCards/package.json b/Node/cards-AdaptiveCards/package.json new file mode 100644 index 0000000000..e0a99660c3 --- /dev/null +++ b/Node/cards-AdaptiveCards/package.json @@ -0,0 +1,31 @@ +{ + "name": "botbuilder-sample-adaptivecards", + "version": "1.0.0", + "description": "Bot Builder Sample - Adaptive Cards", + "scripts": { + "start": "node app.js" + }, + "author": "Microsoft Corp.", + "license": "MIT", + "keywords": [ + "botbuilder", + "bots", + "chatbots", + "botbuilder-samples", + "adaptive-cards" + ], + "bugs": { + "url": "https://github.com/Microsoft/BotBuilder-Samples/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/BotBuilder-Samples.git" + }, + "dependencies": { + "bluebird": "^3.5.0", + "botbuilder": "^3.8.3", + "dotenv-extended": "^2.0.0", + "lodash": "^4.17.4", + "restify": "^4.3.0" + } +} diff --git a/Node/cards-AdaptiveCards/store.js b/Node/cards-AdaptiveCards/store.js new file mode 100644 index 0000000000..d2c6df7547 --- /dev/null +++ b/Node/cards-AdaptiveCards/store.js @@ -0,0 +1,32 @@ +var Promise = require('bluebird'); + +module.exports = { + searchHotels: function (destination, checkInDate, checkOutDate) { + return new Promise(function (resolve) { + + // Filling the hotels results manually just for demo purposes + var hotels = []; + for (var i = 1; i <= 6; i++) { + hotels.push({ + name: 'Hotel ' + i, + location: destination, + rating: Math.ceil(Math.random() * 5), + numberOfReviews: Math.floor(Math.random() * 5000) + 1, + priceStarting: Math.floor(Math.random() * 450) + 80, + image: 'https://placeholdit.imgix.net/~text?txtsize=35&txt=Hotel+' + i + '&w=500&h=260', + moreImages: [ + 'https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+1&w=450&h=300', + 'https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+2&w=450&h=300', + 'https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+3&w=450&h=300', + 'https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+4&w=450&h=300' + ] + }); + } + + hotels.sort(function (a, b) { return a.priceStarting - b.priceStarting; }); + + // complete promise with a timer to simulate async response + setTimeout(function () { resolve(hotels); }, 1000); + }); + } +}; \ No newline at end of file diff --git a/Node/cards-AdaptiveCards/support.js b/Node/cards-AdaptiveCards/support.js new file mode 100644 index 0000000000..72b9d0adcf --- /dev/null +++ b/Node/cards-AdaptiveCards/support.js @@ -0,0 +1,13 @@ +module.exports = function (session) { + // Generate ticket + var tickerNumber = Math.ceil(Math.random() * 20000); + + // Reply and return to parent dialog + session.send('Your message \'%s\' was registered. Once we resolve it; we will get back to you.', session.message.text); + + session.send('Thanks for contacting our support team. Your ticket number is %s.', tickerNumber); + + session.endDialogWithResult({ + response: tickerNumber + }); +}; \ No newline at end of file