From 7d79c03d1548bc5059d7a5be0ced02de116809e2 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Thu, 11 Jan 2018 04:24:16 +0100 Subject: [PATCH] initial step 4 for online test --- backend/express.js | 129 ++++++++++++++++++++++++++--- backend/helpers/amazon.js | 41 ++++++++++ web/src/App.js | 134 ++++++++++++++++++------------- web/src/components/Contact.js | 48 +++++++++++ web/src/components/IntentList.js | 11 ++- web/src/config.js | 6 ++ web/src/css/Intent.css | 14 ++++ web/src/css/Intent.scss | 16 ++++ 8 files changed, 328 insertions(+), 71 deletions(-) create mode 100644 web/src/components/Contact.js diff --git a/backend/express.js b/backend/express.js index 4177ee6..3a32745 100644 --- a/backend/express.js +++ b/backend/express.js @@ -12,8 +12,19 @@ const router = express.Router (); var app = express (); +//User data for sending message, this is skill-related and will be in some skill container +var Name = null; +var Email = null; +var Message = null; +var State = 0; // states should be defined in seperate file. (Not sending message, Waiting for name, Waiting for email, Waiting for message) +//For now : +// 0 : Not sending Message +// 1 : Waiting for name +// 2 : Waiting for email +// 3 : Waiting for message + // ALWAYS setup the alexa app and attach it to express before anything else. -var alexaApp = new alexa.app ('step3'); // this means we still work with one skill +var alexaApp = new alexa.app ('saburly'); // this means we still work with one skill alexaApp.express ({ expressApp: app, @@ -39,6 +50,7 @@ var updateIntentsJSON = function () { .loadSkill (config.SKILL_DB_ID) .then (skill => { skill.intents.map (intent => { + alexaApp.intent ( intent.intentName, { @@ -50,9 +62,93 @@ var updateIntentsJSON = function () { } ); }); + alexaApp.launch ((request, response) => { return response.say (skill.invocationAnswer).shouldEndSession(false); }); + + alexaApp.intent('EmailIntentLaunch',{ + slots:[], + utterances: [ + 'I want to send a message', + 'I would like to send a message', + 'I would like to leave a message', + 'Leave a message' + ] + }, + function(request,response){ + Name = null; + Email = null; + Message = null; + State = 1; + + return response.say('Ok. What is your name').shouldEndSession(false); + } + ); + + //TODO : Watch out for this intent. It will make trouble with other regular intents + //if other intents have utterance with just one slot like {Data} + //It should be taken care somwhere before this, to check if Email Intent is invoked + //This is problem only if we introduce slot options for regular intents for users + alexaApp.intent('EmailIntent',{ + slots:{ + 'Name':'AMAZON.Person', + 'Email' : 'AMAZON.LITERAL', + 'Message': 'AMAZON.LITERAL', + 'Data': 'AMAZON.LITERAL' + }, + utterances: [ + 'My name is {Name}', + 'I am {Name}', + '{Data}', + 'My email is {Email}', + 'Send replay to {Email}', + 'My message is {Message}' + ] + }, + function(request,response){ + if (!Name) Name = response.slots['Name']; + if (!Email) Email = response.slots['Email']; + if (!Message) Message = response.slots['Message']; + let Data = reponse.slots['Data']; + + //TODO : Responses could be configurable for each skill ? + + if (State === 1){ + //Was waiting for name, so if Name is null, name is probably in Data + if ((!Name && Data) || Name){ + //got the name, let's continue for the email + State = 2; + return response.say('Ok ' + Name + '. What is your email ?').shouldEndSession(false); + }else{ + //Something is wrong, ask for name again + return response.say('Sorry, I didnt understand your name. Can you say it again ?').shouldEndSession(false); + } + }else if (State === 2){ + //was waiting for email, so if Email is null, email is probably in Data + if ((!Email && Data) || Email){ + //Got the email, first verify email and than continue to message + + //TODO : verify email + State = 3; + return response.say('Great. Whats the message ?').shouldEndSession(false); + }else{ + //Something is wrong, ask for the email again + return response.say('Sorry, I didnt understan you email. Can you say it again ?').shouldEndSession(false); + } + }else if (State === 3){ + //Was waiting for message, so if Message is null, message is probably in Data + if ((!Message && Data) || Message){ + //Ok, we got all informations. Exit email intent + State = 0; + //TODO : Send email + return response.say('Message sent. Someone will contact you ASAP'); + } + } + + } + ); + }) .catch (err => { console.log (err); @@ -92,14 +188,15 @@ router.get ('/deleteSkill/:skillID', async (req, res, next) => { router.post ('/updateSkill/:id', async (req, res, next) => { let id = req.params.id; - let skill = req.body; + let dataFromWeb = JSON.stringify(req.body); + let skill = JSON.parse(dataFromWeb); + let updateOnAmazon = skill.updateOnAmazon; + delete skill.updateOnAmazon; delete skill._id; console.log('id = ' + id); if (id !== '-1') { - amazonHelper - .updateSkill (skill) - .then (amazonResult => { - console.log('amazon result : ' + amazonResult); + if (updateOnAmazon){ + amazonHelper.updateSkill(skill).then(amazonResult=>{ if (amazonResult === 200 || amazonResult === 202) { //Skill uploaded, it's ok to update databaseI databaseHelper @@ -109,16 +206,24 @@ router.post ('/updateSkill/:id', async (req, res, next) => { updateIntentsJSON (); }) .catch (e => { - res.json ({result: -1, message: 'ok'}); + res.json ({result: -1, message: 'error'}); }); - } else { - res.json ({result: -1, message: 'Amazon result ' + amazonResult}); } - }) - .catch (e => { - //skill upload went wrong, don't update database, send error + }).catch(e=>{ res.json ({result: -1, message: e}); }); + }else{ + databaseHelper + .updateSkill (id, skill) + .then (result => { + res.json ({result: 0, message: 'ok'}); + updateIntentsJSON (); + }) + .catch (e => { + res.json ({result: -1, message: 'error'}); + }); + } + } else { //no new skills for now } diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 26ee7e2..24900e0 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -81,6 +81,47 @@ var generateInteractionModel = function (skill) { 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' + ] + }); + allIntents.push({ + name: 'EmailIntent', + slots:[ + { + name: 'Name', + type: 'AMAZON.Person' + }, + { + name: 'Email', + type: 'AMAZON.LITERAL' + }, + { + name: 'Message', + type: 'AMAZON.LITERAL' + }, + { + name: 'Data', + type: 'AMAZON.LITERAL' + } + ], + samples: [ + 'My name is {Name}', + 'I am {Name}', + '{Data}', + 'My email is {Email}', + 'Send replay to {Email}', + 'My message is {Message}' + ] + }); + result.interactionModel = {}; result.interactionModel.languageModel = { diff --git a/web/src/App.js b/web/src/App.js index 856efac..1a08693 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -4,9 +4,13 @@ import './css/popup.css'; import IntentList from './components/IntentList'; 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 { + NEW_INTENT_SELECTED_INDEX, + LAUNCH_REQUEST_SELECTED_INDEX, + CONTACT_SELECTED_INDEX} from './config' class App extends Component { @@ -20,8 +24,8 @@ class App extends Component { invocationAnswer:'We are saburly', allIntents:[], selectedIntent: {intentName:'',questions:[''],answer:''}, - selectedIndex:-1, - launchRequest:false, + selectedIndex:NEW_INTENT_SELECTED_INDEX, + contactEmail:'', waiting: false }; @@ -30,7 +34,7 @@ class App extends Component { if (jResult===undefined) return; this.setState({ skillID:jResult.skillID,skillName:jResult.skillName, invocationName: jResult.invocationName, invocationAnswer: jResult.invocationAnswer, - allIntents: jResult.intents}) + allIntents: jResult.intents, contactEmail: jResult.contactEmail}) }) this.handleIntentClick = this.handleIntentClick.bind(this); @@ -41,62 +45,67 @@ class App extends Component { 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() { - - if(this.state.launchRequest){ - return ( -
- -
-

Tell All

-
- - - - + return( +
+ +
+

Tell All

- ); - }else{ - return ( -
- -
-

Tell All

-
- - - - -
- ); - } + + + {( + ()=>{ + if (this.state.selectedIndex===LAUNCH_REQUEST_SELECTED_INDEX){ + return ( + + + ); + }else if (this.state.selectedIndex===CONTACT_SELECTED_INDEX){ + return ( + + + ); + }else{ + return( + + + ); + } + } + )()} +
+ ); } - createSkill(intents, name, answer){ + createSkill(intents, name, answer, email, updateOnAmazon){ return { _id: this.state._id, skillID: this.state.skillID, intents: intents, - invocationName: (name===undefined) ? this.state.invocationName : name, - invocationAnswer: (answer===undefined)? this.state.invocationAnswer: answer + invocationName: name, + invocationAnswer: answer, + contactEmail: email, + updateOnAmazon: updateOnAmazon }; } @@ -105,12 +114,21 @@ class App extends Component { } handleLaunchRequestClick(){ - this.setState({selectedIndex: -2, launchRequest:true}); + this.setState({selectedIndex: LAUNCH_REQUEST_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.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); } handleDeleteIntentClick(selectedIntent){ @@ -128,7 +146,7 @@ class App extends Component { this.setState({waiting:true}); let newState = {allIntents: newAllIntents, selectedIntent: {intentName:'', questions:[''],answer:''}, waiting:false}; - this.sendSkill(newAllIntents,true,newState,{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); @@ -142,7 +160,7 @@ class App extends Component { let newAllIntents = JSON.parse(newAllIntentsJSON); let newState = null; - if (this.state.selectedIndex === -1){ + if (this.state.selectedIndex === NEW_INTENT_SELECTED_INDEX){ //new intent newAllIntents.push(selectedIntent); newState = {allIntents: newAllIntents, selectedIntent: selectedIntent, selectedIndex: newAllIntents.length-1, waiting:false}; @@ -151,16 +169,16 @@ class App extends Component { newState = {allIntents: newAllIntents, selectedIntent: selectedIntent, waiting: false}; } this.setState({waiting:true}); - this.sendSkill(newAllIntents, true, newState, {waiting:false}); + 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: -1,launchRequest:false,selectedIntent: {intentName:'',questions:[''], answer:''}}); + this.setState({allIntents: this.state.allIntents, selectedIndex: NEW_INTENT_SELECTED_INDEX,launchRequest:false,selectedIntent: {intentName:'',questions:[''], answer:''}}); } - sendSkill(newAllIntents, showPopUp, resolveState, rejectState, newName, newAnswer){ + sendSkill(newAllIntents, showPopUp, resolveState, rejectState, newName, newAnswer, email, updateOnAmazon){ return new Promise((resolve,reject)=>{ - updateSkill(this.createSkill(newAllIntents,newName,newAnswer)).then(l=>l.text()).then(result=>{ + updateSkill(this.createSkill(newAllIntents,newName,newAnswer,email,updateOnAmazon)).then(l=>l.text()).then(result=>{ let jResult = JSON.parse(result); if (jResult.result !== 0){ if (showPopUp) Popup.alert('Model was not saved. Please try again'); diff --git a/web/src/components/Contact.js b/web/src/components/Contact.js new file mode 100644 index 0000000..31e6b60 --- /dev/null +++ b/web/src/components/Contact.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react'; +import {Button, TextField} from 'react-md'; +import '../css/Intent.css' +import {EMAIL_MAX_LENGTH} from '../config'; + +class Contact extends Component { + constructor(props){ + super(props); + + this.state = {contactEmail: props.contactEmail}; + + this.handleEmailEdit = this.handleEmailEdit.bind(this); + } + + componentWillReceiveProps(props){ + this.setState({contactEmail: props.contactEmail}); + } + + render() { + return ( +
+
Contact address will be used for direct messaging through Alexa
+ +

+

+

+ +
+ ); + } + + handleEmailEdit(e){ + if (e.length === EMAIL_MAX_LENGTH) return; + this.setState({contactEmail: e}); + } +} + +export default Contact; diff --git a/web/src/components/IntentList.js b/web/src/components/IntentList.js index 53943c3..e77032a 100644 --- a/web/src/components/IntentList.js +++ b/web/src/components/IntentList.js @@ -2,6 +2,9 @@ import React, { Component } from 'react'; import {Button} from 'react-md'; import IntentItem from './IntentItem'; import '../css/Intent.css' +import { + LAUNCH_REQUEST_SELECTED_INDEX, + CONTACT_SELECTED_INDEX} from '../config' class IntentList extends Component { constructor (props){ @@ -17,9 +20,15 @@ class IntentList extends Component { render() { return (
- + + + +

Intents

diff --git a/web/src/config.js b/web/src/config.js index 1a4de76..ac40a2b 100644 --- a/web/src/config.js +++ b/web/src/config.js @@ -9,3 +9,9 @@ export const INTENT_TITLE_TOOLTIP_DELAY = 700; export const INVOCATION_NAME_MAX_LENGTH = 15; export const INVOCATION_ANSWER_MAX_LENGTH = 100; + +export const EMAIL_MAX_LENGTH = 100; + +export const NEW_INTENT_SELECTED_INDEX = -1; +export const LAUNCH_REQUEST_SELECTED_INDEX = -2; +export const CONTACT_SELECTED_INDEX = -3; \ No newline at end of file diff --git a/web/src/css/Intent.css b/web/src/css/Intent.css index 2556df7..bd94c64 100644 --- a/web/src/css/Intent.css +++ b/web/src/css/Intent.css @@ -24,6 +24,20 @@ height: 50px; background-color: #f5f5f5; } +.Contact { + text-align: left; + color: #009b8a; + width: 100%; + height: 50px; + background-color: #d8d8d8; } + +.Contact-selected { + text-align: left; + color: #009b8a; + width: 100%; + height: 50px; + background-color: #f5f5f5; } + .AddIntent { float: right; margin: 12px; } diff --git a/web/src/css/Intent.scss b/web/src/css/Intent.scss index d755d26..3b35a99 100644 --- a/web/src/css/Intent.scss +++ b/web/src/css/Intent.scss @@ -31,6 +31,22 @@ $minHeight : calc(100vh - 80px); background-color: #f5f5f5; } +.Contact{ + text-align: left; + color: #009b8a; + width: 100%; + height: 50px; + background-color: #d8d8d8 +} + +.Contact-selected{ + text-align: left; + color: #009b8a; + width: 100%; + height: 50px; + background-color: #f5f5f5; +} + .AddIntent{ float: right; margin: 12px;