diff --git a/README.md b/README.md index caec0bf..fc36472 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Prerequests for step 3 (run on server): requires running mongodb service Database (tellall) with collection (skill_list) - * Insert dummy skill with : db.skill_list.insert({"skillID" : "amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae", "intents" : [ { "intentName" : "GetFirstQuestion", "questions" : [ "tell me something about projects", "tell me all about projects" ], "answer" : "blablabla bla bla" }, { "intentName" : "GetThirdQuestion", "questions" : [ "Give me third question" ], "answer" : "This is answer to the third question" } ], "invocationName" : "Saburly", "invocationAnswer" : "We are Saburly team one" }) + * Insert dummy skill with : db.skill_list.insert({"skillID" : "amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae", "intents" : [ { "intentName" : "GetFirstQuestion", "questionExplanation" : "", "questions" : [ "tell me something about projects", "tell me all about projects" ], "answer" : "blablabla bla bla" }, { "intentName" : "GetThirdQuestion", "questionExplanation" : "", "questions" : [ "Give me third question" ], "answer" : "This is answer to the third question" } ], "invocationName" : "Saburly", "invocationAnswer" : "We are Saburly team one" }) *obtain _id and change in web/src/App.js, and also skill_db_id in backend/config.js *enter web/ dir and run "npm run build" diff --git a/backend/config/constants.js b/backend/config/constants.js index 6674956..7c3a7df 100644 --- a/backend/config/constants.js +++ b/backend/config/constants.js @@ -17,7 +17,7 @@ constants.apiResultCodes = { AMAZON_FAIL:2, //amazon api doesn't work DATABASE_ERROR:3, NO_SKILL:4, - INCONSISTEN_STATE:5, + INCONSISTENT_STATE:5, } constants.HTTPResultCodes = { @@ -27,10 +27,16 @@ constants.HTTPResultCodes = { constants.SKILL_ID_LENGTH = 24; constants.voiceResponseStrings = { - QUESTION_NOT_FOUND : 'Sorry, I didnt understan', + QUESTION_NOT_FOUND : 'Sorry, I didnt understand', GENERIC_CONTINUE : 'Would you like to continue' } +//Timing is given in [ms] +constants.voiceResponseTimings = { + PAUSE_BETWEEN_QUESTIONS : 650, + PAUSE_AFTER_WELCOME_MESSAGE : 650, +} + module.exports = constants; \ No newline at end of file diff --git a/backend/config/email.js b/backend/config/email.js new file mode 100644 index 0000000..89e0ddc --- /dev/null +++ b/backend/config/email.js @@ -0,0 +1,14 @@ +var config = {}; + +config.PORT = 587; +config.SMTP_HOST = 'smtp.mail.com'; +config.SECURE = false; +config.AUTH = { + user: 'saburly@mail.com', + pass: 'KeepSaburly', +}; + +config.FROM_EMAIL = 'saburly@mail.com'; +config.SUBJECT = 'Message from Saburly service'; + +module.exports = config; diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js index 6cc36e7..a0c2f5f 100644 --- a/backend/controllers/skill.js +++ b/backend/controllers/skill.js @@ -41,6 +41,7 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { .then (() => { //Ok, done, now update skill on Amazon (if needed) if (updateOnAmazon) { + //We need to update skill on Amazon amazonHelper .updateSkill (skill) .then (amazonResult => { @@ -51,45 +52,78 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { res.json ({result: constants.apiResultCodes.OK, message: ''}); alexa.updateModel (); } else { - res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ - result: constants.apiResultCodes.AMAZON_ERROR, - message: amazonResult, - }); + //Update on amazon failed, revert changes in database and send error to user + databaseHelper + .updateSkill (id, currentSkillState) + .then (() => { + res + .status ( + constants.HTTPResultCodes.INTERNAL_SERVER_ERROR + ) + .json ({ + result: constants.apiResultCodes.AMAZON_ERROR, + message: amazonResult, + }); + }) + .catch (() => { + //This should never happen, something is seriously wrong, like no database connection + res + .status ( + constants.HTTPResultCodes.INTERNAL_SERVER_ERROR + ) + .json ({ + result: constants.apiResultCodes.INCONSISTENT_STATE, + message: '', + }); + }); } }) .catch (e => { - res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ - result: constants.apiResultCodes.AMAZON_FAIL, - message: e, - }); + //Update on amazon failed, revert changes in database and send error to user + databaseHelper + .updateSkill (id, currentSkillState) + .then (() => { + res + .status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR) + .json ({ + result: constants.apiResultCodes.AMAZON_FAIL, + message: e, + }); + }) + .catch (() => { + //This should never happen, something is seriously wrong, like no database connection + res + .status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR) + .json ({ + result: constants.apiResultCodes.INCONSISTENT_STATE, + message: '', + }); + }); }); - }else{ + } else { + //No need to update on Amazon, tell to user it's ok res.json ({result: constants.apiResultCodes.OK, message: ''}); alexa.updateModel (); } }) .catch (() => { - //Update in database didn't go well, revert changes - databaseHelper - .updateSkill (id, currentSkillState) - .then (() => { - res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ - result: constants.apiResultCodes.DATABASE_ERROR, - message: '', - }); - }) - .catch (() => { - //This should never happen, something is seriously wrong, like no database connection - res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ - result: constants.apiResultCodes.INCONSISTEN_STATE, - message: '', - }); - }); + //Update in database didn't go well, no need to revert since it failed to write in the first place + //just send error to user + res + .status ( + constants.HTTPResultCodes.INTERNAL_SERVER_ERROR + ) + .json ({ + result: constants.apiResultCodes.DATABASE_ERROR, + message: '', + }); }); }) .catch (e => { //I don't know why, but something went wrong, possibly ID of skill is wrong, doesn't exist in DB - res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({result: constants.apiResultCodes.NO_SKILL, message: ''}); + res + .status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR) + .json ({result: constants.apiResultCodes.NO_SKILL, message: ''}); }); }); diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index d6c554b..fcc781e 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -37,15 +37,22 @@ var refreshTokens = function () { request (options, function (error, response, body) { if (error) { reject (error); - }else{ + } else { parsedResponse = JSON.parse (body); - if (parsedResponse.refresh_token){ - databaseHelper.updateTokens(parsedResponse.refresh_token, parsedResponse.access_token, parsedResponse.expires_in).then(()=>{ - resolve(); - }).catch(e=>{ - reject(e); - }); - }else{ + if (parsedResponse.refresh_token) { + databaseHelper + .updateTokens ( + parsedResponse.refresh_token, + parsedResponse.access_token, + parsedResponse.expires_in + ) + .then (() => { + resolve (); + }) + .catch (e => { + reject (e); + }); + } else { reject (body); } } @@ -56,87 +63,186 @@ var refreshTokens = function () { var generateInteractionModel = function (skill) { let result = {}; let allIntents = []; - let defaultIntents = [ - { - name: 'AMAZON.CancelIntent', - samples: [], - }, - { - name: 'AMAZON.HelpIntent', - samples: [], - }, - { - name: 'AMAZON.StopIntent', - samples: [], - }, - ]; - - /* - defaultIntents.map(intent=>{ - allIntents.push(intent); - }); - */ skill.intents.map (intent => { allIntents.push ({name: intent.intentName, samples: intent.questions}); }); - //Special Email Intents : - /* - allIntents.push ({ - name: 'EmailIntentLaunch', - slots: [], - samples: [ - 'I want to send a message', - 'I would like to send a message', - 'I would like to leave a message', - 'Leave a message', - ], - }); + //Special intent for sending message (Dialog) allIntents.push ({ - name: 'EmailIntent', + name: 'SendMessageIntent', + samples: [ + 'I would like to send a message', + 'I want to send a message', + 'Send message', + ], slots: [ { name: 'Name', type: 'AMAZON.US_FIRST_NAME', + samples: ['My name is {Name}', 'I am {Name}', '{Name}'], }, { name: 'Email', - type: 'AMAZON.LITERAL', + type: 'EmailSlot', + samples: ['My email is {Email}', '{Email}'], }, { name: 'Message', - type: 'AMAZON.LITERAL', + type: 'MessageSlot', + samples: ['{Message}'], }, - { - name: 'Data', - type: 'AMAZON.LITERAL', - }, - ], - - samples: [ - 'My name is {Name}', - 'I am {Name}', - '{exampleww at wwdwdw|Data}', - 'My email is {example at efefegedd|Email}', - 'Send replay to {example at abcdefg|Email}', - 'My message is {The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.|Message}', ], }); - */ + + let customSlotTypes = [ + { + name: 'EmailSlot', + values: [ + { + id: null, + name: { + value: 'bla@bla.bla', + synonyms: [], + }, + }, + { + id: null, + name: { + value: 'bla.bla@bla.bla.bla', + synonyms: [], + }, + }, + { + id: null, + name: { + value: 'bla_bla@bla.bla', + synonyms: [], + }, + }, + ], + }, + { + name: 'MessageSlot', + values: [ + { + id: null, + name: { + value: 'Quick brown fox jumps over lazy dog', + synonyms: [], + }, + }, + { + id: null, + name: { + value: 'Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog.', + synonyms: [], + }, + }, + { + id: null, + name: { + value: 'Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog.', + synonyms: [], + }, + }, + ], + }, + ]; + + let dialogPrompts = [ + { + id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Name', + variations: [ + { + type: 'PlainText', + value: 'What is your name ?', + }, + { + type: 'PlainText', + value: 'Tell me your name', + }, + ], + }, + { + id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Email', + variations: [ + { + type: 'PlainText', + value: 'What is your email ?', + }, + { + type: 'PlainText', + value: 'Tell me your email', + }, + ], + }, + { + id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Message', + variations: [ + { + type: 'PlainText', + value: 'What is your message', + }, + ], + }, + ]; + + let dialogIntents = [ + { + name: 'SendMessageIntent', + confirmationRequired: false, + prompts: {}, + slots: [ + { + name: 'Name', + type: 'AMAZON.US_FIRST_NAME', + elicitationRequired: true, + confirmationRequired: false, + prompts: { + elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Name', + }, + }, + { + name: 'Email', + type: 'EmailSlot', + elicitationRequired: true, + confirmationRequired: false, + prompts: { + elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Email', + }, + }, + { + name: 'Message', + type: 'MessageSlot', + elicitationRequired: true, + confirmationRequired: false, + prompts: { + elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Message', + }, + }, + ], + }, + ]; result.interactionModel = {}; result.interactionModel.languageModel = { invocationName: skill.invocationName, + types: customSlotTypes, intents: allIntents, }; + result.interactionModel.prompts = dialogPrompts; + result.interactionModel.dialog = {}; + result.interactionModel.dialog.intents = dialogIntents; + return JSON.stringify (result); }; var uploadSkill = function (skill) { + let generatedInteractionModel = generateInteractionModel (skill); return fetch ( `https://api.amazonalexa.com/v0/skills/${skill.skillID}/interactionModel/locales/en-US`, { @@ -144,7 +250,7 @@ var uploadSkill = function (skill) { headers: { Authorization: config.TOKEN, }, - body: generateInteractionModel (skill), + body: generatedInteractionModel, } ); }; diff --git a/backend/helpers/email.js b/backend/helpers/email.js new file mode 100644 index 0000000..f13a0ce --- /dev/null +++ b/backend/helpers/email.js @@ -0,0 +1,75 @@ +const nodemailer = require ('nodemailer'); +const emailConfig = require('../config/email'); + +module.exports = { + transformEmailFromAlexaResponse: function (email) { + //email from alexa response will contain words instead of symbols, like : + //at = @ + //underscore = _ + //dash = - + //dot = . + //TODO: This list should be longer + let transformedEmail = email + .replace (/\s/g, '') //remove all spaces + .replace (/at/gi, '@') + .replace (/underscore/gi, '_') + .replace (/dash/gi, '-') + .replace (/dot/gi, '.'); + + return transformedEmail; + }, + + isEmailValid: function (email) { + console.log ('Email to validate : ' + email); + let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + + return validEmailRegex.test (email); + }, + + sendEmail: function (name, fromEmail, message, toEmail) { + return new Promise ((resolve, reject) => { + fromEmail = this.transformEmailFromAlexaResponse(fromEmail); + let messageBody = + 'Hello. User left you a message on Saburly service using Alexa skill. \r\nMessage : ' + + message + + '\r\nName : ' + + name + + '\r\nEmail : ' + + fromEmail + + '\r\nYour Saburly team'; + + let messageBodyHTML = + '

Hello. User left you a message on Saburly service using Alexa skill.


Message :

' + + message + + '


Name : ' + + name + + '
Email : ' + + fromEmail + + '

Your Saburly team'; + + let transporter = nodemailer.createTransport ({ + host: emailConfig.SMTP_HOST, + port: emailConfig.PORT, + secure: emailConfig.SECURE, + auth: emailConfig.AUTH, + }); + + var mailOptions = { + from: emailConfig.FROM_EMAIL, + replyTo: fromEmail, + to: toEmail, + subject: emailConfig.SUBJECT, + text: messageBody, + html: messageBodyHTML, + }; + + transporter.sendMail (mailOptions, (error, info) => { + if (error) { + reject (error); + } else { + resolve (info); + } + }); + }); + }, +}; diff --git a/backend/models/alexa.js b/backend/models/alexa.js index d95c855..8a31a0d 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -1,16 +1,17 @@ var Alexa = require ('alexa-sdk'); const config = require ('../config/config'); var databaseHelper = require ('../helpers/database'); +var emailHelper = require ('../helpers/email'); const constants = require ('../config/constants'); var handlers = {}; +var destinationEmail; module.exports = { run: function (req, res) { // Build the context manually, because Amazon Lambda is missing var context = { succeed: function (result) { - console.log (result); res.json (result); }, fail: function (error) { @@ -30,15 +31,34 @@ module.exports = { .getSkill (config.SKILL_DB_ID) .then (activeSkill => { handlers = {}; + destinationEmail = activeSkill.contactEmail; - //Handler for launch request + let listOfPossibleQuestions = ''; + activeSkill.intents.forEach(intent => { + if (intent.questions.length > 0 && intent.intentExplanation) { + listOfPossibleQuestions += + intent.intentExplanation + + intent.questions[0] + + ''; + } + }); + + console.log(listOfPossibleQuestions); + + //Handler for launch requestconsole.log() handlers = { LaunchRequest: function () { this.response - .speak (activeSkill.invocationAnswer) + .speak ( + activeSkill.invocationAnswer + + '' + + listOfPossibleQuestions + ) .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! - //TODO : Maybe to ask user does he want to hear possible intents - //For this functionality, we need explanation text for each intent (Question) this.emit (':responseReady'); }, }; @@ -53,19 +73,105 @@ module.exports = { }; }); + //Handler for sending message + handlers.SendMessageIntent = function () { + let intent = this.event.request.intent; + + console.log ('Dialog state : ' + this.event.request.dialogState); + console.log (intent); + //STARTED, IN_PROGRESS + + if (!intent.slots.Name.value) { + //Name not defined yet, ask user for name + const slotToElicit = 'Name'; + const speechOutput = 'What is your name'; + const repromptSpeech = speechOutput; + this.emit ( + ':elicitSlot', + slotToElicit, + speechOutput, + repromptSpeech + ); + } else if (!intent.slots.Email.value) { + //Name not defined yet, ask user for email + const slotToElicit = 'Email'; + const speechOutput = + 'Ok ' + intent.slots.Name.value + '. What is your email'; + const repromptSpeech = speechOutput; + this.emit ( + ':elicitSlot', + slotToElicit, + speechOutput, + repromptSpeech + ); + } else if (!intent.slots.Message.value) { + intent.slots.Email.value = emailHelper.transformEmailFromAlexaResponse ( + intent.slots.Email.value + ); + if (!emailHelper.isEmailValid (intent.slots.Email.value)) { + //Email is not valid, ask again + const slotToElicit = 'Email'; + const speechOutput = + 'Sorry, that was not valid email. What is your email'; + const repromptSpeech = speechOutput; + this.emit ( + ':elicitSlot', + slotToElicit, + speechOutput, + repromptSpeech + ); + } else { + //Email is valid + const slotToElicit = 'Message'; + const speechOutput = 'Great. What is your message'; + const repromptSpeech = speechOutput; + this.emit ( + ':elicitSlot', + slotToElicit, + speechOutput, + repromptSpeech + ); + } + } else { + //all slots are filled + console.log ('Name : ' + intent.slots.Name.value); + console.log ('Email : ' + intent.slots.Email.value); + console.log ('Message : ' + intent.slots.Message.value); + emailHelper + .sendEmail ( + intent.slots.Name.value, + intent.slots.Email.value, + intent.slots.Message.value, + destinationEmail + ) + .then (info => { + console.log (info); + this.response.speak ( + 'Ok. Message sent. Someone will contact you ASAP' + ); + this.emit (':responseReady'); + }) + .catch (error => { + console.log (error); + this.response.speak ( + 'Sorry, there was a problem with sending message.' + ); + this.emit (':responseReady'); + }); + } + }; + //Default handlers for unknown questions and session close handlers.Unhandled = function () { this.response .speak (constants.voiceResponseStrings.QUESTION_NOT_FOUND) .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); - } + }; - handlers.SessionEndedRequest = function(){ + handlers.SessionEndedRequest = function () { //We don't care for now - } - - + }; }) .catch (e => { //Something is wrong, skill is not ready, use catch-all intent to inform user diff --git a/backend/package.json b/backend/package.json index b0c0319..3b48add 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,6 +10,7 @@ "express": "^4.13.0", "isomorphic-fetch": "^2.2.1", "mongodb": "^2.2.33", + "nodemailer": "^4.4.1", "request": "^2.83.0" }, "author": "Matt Kruse (http://mattkruse.com/)", diff --git a/web/src/App.js b/web/src/App.js index 663d0ec..a91c5f6 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, {Component} from 'react'; import './css/App.css'; import './css/popup.css'; import IntentList from './components/IntentList'; @@ -6,90 +6,117 @@ import IntentDetails from './components/IntentDetails'; import LaunchRequest from './components/LaunchRequest'; import Contact from './components/Contact'; import Popup from 'react-popup'; -import {getSkill, updateSkill} from './lib/api' +import {getSkill, updateSkill} from './lib/api'; import { - NEW_INTENT_SELECTED_INDEX, - LAUNCH_REQUEST_SELECTED_INDEX, - CONTACT_SELECTED_INDEX, - RESULT_CODES} from './config/constants' + NEW_INTENT_SELECTED_INDEX, + LAUNCH_REQUEST_SELECTED_INDEX, + CONTACT_SELECTED_INDEX, + RESULT_CODES, +} from './config/constants'; class App extends Component { + constructor (props) { + super (props); - constructor(props){ - super(props); + this.state = { + _id: '5a232fb86ce046c749739455', + skillID: '', + skillName: '', + invocationName: 'Saburly', + invocationAnswer: 'We are saburly', + allIntents: [], + selectedIntent: { + intentName: '', + intentExplanation: '', + questions: [''], + answer: '', + }, + selectedIndex: NEW_INTENT_SELECTED_INDEX, + contactEmail: '', + waiting: false, + }; - this.state={_id:'5a232fb86ce046c749739455', - skillID:'', - skillName:'', - invocationName:'Saburly', - invocationAnswer:'We are saburly', - allIntents:[], - selectedIntent: {intentName:'',questions:[''],answer:''}, - selectedIndex:NEW_INTENT_SELECTED_INDEX, - contactEmail:'', - waiting: false - }; + getSkill (this.state._id).then (l => l.json ()).then (result => { + if (result === undefined) return; + this.setState ({ + skillID: result.skillID, + skillName: result.skillName, + invocationName: result.invocationName, + invocationAnswer: result.invocationAnswer, + allIntents: result.intents, + contactEmail: result.contactEmail, + }); + }); - getSkill(this.state._id).then(l=>l.json()).then(result=>{ - if (result===undefined) return; - this.setState({ skillID:result.skillID,skillName:result.skillName, invocationName: result.invocationName, - invocationAnswer: result.invocationAnswer, - allIntents: result.intents, contactEmail: result.contactEmail}) - }) - - this.handleIntentClick = this.handleIntentClick.bind(this); - this.handleLaunchRequestClick = this.handleLaunchRequestClick.bind(this); - this.handleDeleteIntentClick = this.handleDeleteIntentClick.bind(this); - this.handleSaveIntentClick = this.handleSaveIntentClick.bind(this); - this.handleAddIntentClick = this.handleAddIntentClick.bind(this); - this.handleSaveLaunchRequestClick = this.handleSaveLaunchRequestClick.bind(this); - this.createSkill = this.createSkill.bind(this); - this.sendSkill = this.sendSkill.bind(this); - this.handleContactClick = this.handleContactClick.bind(this); - this.handleSaveEmailClick = this.handleSaveEmailClick.bind(this); + this.handleIntentClick = this.handleIntentClick.bind (this); + this.handleLaunchRequestClick = this.handleLaunchRequestClick.bind (this); + this.handleDeleteIntentClick = this.handleDeleteIntentClick.bind (this); + this.handleSaveIntentClick = this.handleSaveIntentClick.bind (this); + this.handleAddIntentClick = this.handleAddIntentClick.bind (this); + this.handleSaveLaunchRequestClick = this.handleSaveLaunchRequestClick.bind ( + this + ); + this.createSkill = this.createSkill.bind (this); + this.sendSkill = this.sendSkill.bind (this); + this.handleContactClick = this.handleContactClick.bind (this); + this.handleSaveEmailClick = this.handleSaveEmailClick.bind (this); } - render() { + render () { let rightPanel; switch (this.state.selectedIndex) { case LAUNCH_REQUEST_SELECTED_INDEX: - rightPanel = ; + rightPanel = ( + + ); break; case CONTACT_SELECTED_INDEX: - rightPanel = ; + rightPanel = ( + + ); break; default: - rightPanel = ; + rightPanel = ( + + ); } - return( + return (
- +

Tell All

- - + + {rightPanel}
); } - createSkill(intents, name, answer, email, updateOnAmazon){ + createSkill (intents, name, answer, email, updateOnAmazon) { return { _id: this.state._id, skillID: this.state.skillID, @@ -97,97 +124,181 @@ class App extends Component { invocationName: name, invocationAnswer: answer, contactEmail: email, - updateOnAmazon: updateOnAmazon + updateOnAmazon: updateOnAmazon, }; } - handleIntentClick(selectedIntent, index){ - this.setState({selectedIntent:selectedIntent, selectedIndex: index, launchRequest:false}); + handleIntentClick (selectedIntent, index) { + this.setState ({ + selectedIntent: selectedIntent, + selectedIndex: index, + launchRequest: false, + }); } - handleLaunchRequestClick(){ - this.setState({selectedIndex: LAUNCH_REQUEST_SELECTED_INDEX}); + handleLaunchRequestClick () { + this.setState ({selectedIndex: LAUNCH_REQUEST_SELECTED_INDEX}); } - handleContactClick(){ - this.setState({selectedIndex: CONTACT_SELECTED_INDEX}) + handleContactClick () { + this.setState ({selectedIndex: CONTACT_SELECTED_INDEX}); } - handleSaveLaunchRequestClick(name, answer){ - this.setState({waiting:true, invocationName:name, invocationAnswer: answer}); - this.sendSkill(this.state.allIntents,true,{waiting:false},{waiting:false},name,answer,this.state.contactEmail,true); + handleSaveLaunchRequestClick (name, answer) { + this.setState ({ + waiting: true, + invocationName: name, + invocationAnswer: answer, + }); + this.sendSkill ( + this.state.allIntents, + true, + {waiting: false}, + {waiting: false}, + name, + answer, + this.state.contactEmail, + true + ); } - handleSaveEmailClick(email){ - this.setState({waiting:true}); - this.sendSkill(this.state.allIntents,true,{contactEmail: email, waiting:false},{waiting:false},this.state.invocationName,this.state.invocationAnswer,email,false); + handleSaveEmailClick (email) { + this.setState ({waiting: true}); + this.sendSkill ( + this.state.allIntents, + true, + {contactEmail: email, waiting: false}, + {waiting: false}, + this.state.invocationName, + this.state.invocationAnswer, + email, + false + ); } - handleDeleteIntentClick(selectedIntent){ + handleDeleteIntentClick (selectedIntent) { let id = -1; //TODO : Change comparsion method ! Same object with different proeprty sorting will not be same string - this.state.allIntents.map((intent,index)=>{ - if ((id===-1) && (JSON.stringify(selectedIntent)===JSON.stringify(intent))) - id = index; + this.state.allIntents.map ((intent, index) => { + if ( + id === -1 && + JSON.stringify (selectedIntent) === JSON.stringify (intent) + ) + id = index; }); - if (id!==-1){ - try{ - let newAllIntentsJSON = JSON.stringify(this.state.allIntents); - let newAllIntents = JSON.parse(newAllIntentsJSON); - newAllIntents.splice(id,1); - this.setState({waiting:true}); + if (id !== -1) { + try { + let newAllIntentsJSON = JSON.stringify (this.state.allIntents); + let newAllIntents = JSON.parse (newAllIntentsJSON); + newAllIntents.splice (id, 1); + this.setState ({waiting: true}); - let newState = {allIntents: newAllIntents, selectedIntent: {intentName:'', questions:[''],answer:''}, waiting:false}; - this.sendSkill(newAllIntents,true,newState,{waiting:false},this.state.invocationName,this.state.invocationAnswer,this.state.contactEmail,true); - - }catch(e){ - console.log("error : " + e); + let newState = { + allIntents: newAllIntents, + selectedIntent: {intentName: '', questions: [''], answer: ''}, + waiting: false, + }; + this.sendSkill ( + newAllIntents, + true, + newState, + {waiting: false}, + this.state.invocationName, + this.state.invocationAnswer, + this.state.contactEmail, + true + ); + } catch (e) { + console.log ('error : ' + e); } } } - - handleSaveIntentClick(selectedIntent){ - let newAllIntentsJSON = JSON.stringify(this.state.allIntents); - let newAllIntents = JSON.parse(newAllIntentsJSON); + handleSaveIntentClick (selectedIntent) { + let newAllIntentsJSON = JSON.stringify (this.state.allIntents); + let newAllIntents = JSON.parse (newAllIntentsJSON); let newState = null; - if (this.state.selectedIndex === NEW_INTENT_SELECTED_INDEX){ + if (this.state.selectedIndex === NEW_INTENT_SELECTED_INDEX) { //new intent - newAllIntents.push(selectedIntent); - newState = {allIntents: newAllIntents, selectedIntent: selectedIntent, selectedIndex: newAllIntents.length-1, waiting:false}; - }else{ + newAllIntents.push (selectedIntent); + newState = { + allIntents: newAllIntents, + selectedIntent: selectedIntent, + selectedIndex: newAllIntents.length - 1, + waiting: false, + }; + } else { newAllIntents[this.state.selectedIndex] = selectedIntent; - newState = {allIntents: newAllIntents, selectedIntent: selectedIntent, waiting: false}; + newState = { + allIntents: newAllIntents, + selectedIntent: selectedIntent, + waiting: false, + }; } - this.setState({waiting:true}); - this.sendSkill(newAllIntents, true, newState, {waiting:false}, this.state.invocationName,this.state.invocationAnswer,this.state.contactEmail, true); + this.setState ({waiting: true}); + this.sendSkill ( + newAllIntents, + true, + newState, + {waiting: false}, + this.state.invocationName, + this.state.invocationAnswer, + this.state.contactEmail, + true + ); } - handleAddIntentClick(){ - this.setState({allIntents: this.state.allIntents, selectedIndex: NEW_INTENT_SELECTED_INDEX,launchRequest:false,selectedIntent: {intentName:'',questions:[''], answer:''}}); + handleAddIntentClick () { + this.setState ({ + allIntents: this.state.allIntents, + selectedIndex: NEW_INTENT_SELECTED_INDEX, + launchRequest: false, + selectedIntent: {intentName: '', questions: [''], answer: '', intentExplanation:''}, + }); } - sendSkill(newAllIntents, showPopUp, resolveState, rejectState, newName, newAnswer, email, updateOnAmazon){ - return new Promise((resolve,reject)=>{ - updateSkill(this.createSkill(newAllIntents,newName,newAnswer,email,updateOnAmazon)).then(l=>l.json()).then(result=>{ - if (result.result !== RESULT_CODES.OK){ - console.log(result.result); - if (showPopUp) Popup.alert('Model was not saved. Please try again'); - this.setState(rejectState); - //reject('Error code : ' + jResult.result); - }else{ - if (showPopUp) Popup.alert('Saved'); - this.setState(resolveState); - resolve(); - } - }).catch(e=>{ - console.log('error : ' + e); - if (showPopUp) Popup.alert('Model was not saved. Please try again'); - this.setState(rejectState); - //reject(e); - }); + sendSkill ( + newAllIntents, + showPopUp, + resolveState, + rejectState, + newName, + newAnswer, + email, + updateOnAmazon + ) { + return new Promise ((resolve, reject) => { + updateSkill ( + this.createSkill ( + newAllIntents, + newName, + newAnswer, + email, + updateOnAmazon + ) + ) + .then (l => l.json ()) + .then (result => { + if (result.result !== RESULT_CODES.OK) { + console.log (result); + if (showPopUp) + Popup.alert ('Model was not saved. Please try again'); + this.setState (rejectState); + //reject('Error code : ' + jResult.result); + } else { + if (showPopUp) Popup.alert ('Saved'); + this.setState (resolveState); + resolve (); + } + }) + .catch (e => { + console.log ('error : ' + e); + if (showPopUp) Popup.alert ('Model was not saved. Please try again'); + this.setState (rejectState); + //reject(e); + }); }); } } diff --git a/web/src/components/IntentDetails.js b/web/src/components/IntentDetails.js index e5c6066..8e76100 100644 --- a/web/src/components/IntentDetails.js +++ b/web/src/components/IntentDetails.js @@ -1,7 +1,8 @@ import React, { Component } from 'react'; import {Button, SVGIcon, TextField} from 'react-md'; import '../css/components/IntentDetails.css'; -import {QUESTION_MAX_LENGTH, ANSWER_MAX_LENGTH, INTENT_NAME_MAX_LENGTH} from '../config/constants'; +import '../css/Common.css'; +import {QUESTION_MAX_LENGTH, ANSWER_MAX_LENGTH, INTENT_NAME_MAX_LENGTH, INTENT_EXPLANATION_MAX_LENGTH} from '../config/constants'; class IntentDetails extends Component { constructor(props){ @@ -14,6 +15,7 @@ class IntentDetails extends Component { this.handleQuestionEdit = this.handleQuestionEdit.bind(this); this.handleAnswerEdit = this.handleAnswerEdit.bind(this); this.handleIntentNameEdit = this.handleIntentNameEdit.bind(this); + this.handleIntentExplanationEdit = this.handleIntentExplanationEdit.bind(this); } componentWillReceiveProps(props){ @@ -24,10 +26,19 @@ class IntentDetails extends Component { return (
+
In introduction, Alexa will help users to ask her the right questions about your business. For Example, she will say : "To ask us about our services, say : What do you do ? ". What do you do ? is defined in question field. Alexa will use first variation of question in intro.
+ +