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 c131195..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 = { @@ -31,6 +31,12 @@ constants.voiceResponseStrings = { 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/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 6d1b553..fcc781e 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -226,24 +226,23 @@ var generateInteractionModel = function (skill) { }, ]; - let dialog = { - intents: dialogIntents, - }; - result.interactionModel = {}; result.interactionModel.languageModel = { invocationName: skill.invocationName, types: customSlotTypes, intents: allIntents, - prompts: dialogPrompts, - dialog: dialog, }; + 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`, { @@ -251,7 +250,7 @@ var uploadSkill = function (skill) { headers: { Authorization: config.TOKEN, }, - body: generateInteractionModel (skill), + body: generatedInteractionModel, } ); }; diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 309e210..8a31a0d 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -12,7 +12,6 @@ module.exports = { // Build the context manually, because Amazon Lambda is missing var context = { succeed: function (result) { - console.log (result); res.json (result); }, fail: function (error) { @@ -34,14 +33,32 @@ module.exports = { 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'); }, }; 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.
+ +