9 Commits

Author SHA1 Message Date
GotPPay
3fe9ebde9d new scss file 2018-01-05 23:59:26 +01:00
GotPPay
b7a778691f install ejs 2018-01-05 23:06:37 +01:00
GotPPay
beb575ef7f remove view engine 2018-01-05 22:53:53 +01:00
GotPPay
c3bba7ffa7 express routes reorder 2018-01-05 20:32:55 +01:00
GotPPay
2990fad6cf package update 2018-01-05 20:15:56 +01:00
GotPPay
03c346f5d2 popup info ; token update 2018-01-05 17:56:24 +01:00
GotPPay
b55dd56859 readme update 2018-01-05 01:00:10 +01:00
GotPPay
32e0f4d6d7 automatic token refresh, initial stage 2018-01-05 00:51:49 +01:00
GotPPay
9652645339 fix 2017-12-04 12:18:05 +01:00
39 changed files with 5251 additions and 254 deletions

12
README.md Normal file
View File

@@ -0,0 +1,12 @@
first terminal
cd web
npm install
npm start
second terminal
cd backend
npm install
node express.js
requires running mongodb service

View File

@@ -0,0 +1,60 @@
{
"languageModel": {
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "GetProcessIntent",
"samples": [
"tell me your process",
"what is your process",
"how do you work",
"about your proces"
],
"slots": []
},
{
"name": "GetProjectsIntent",
"samples": [
"list me your projects",
"show me what you have done",
"what did you do so far",
"what are your projects",
"your project"
],
"slots": []
},
{
"name": "GetServicesIntent",
"samples": [
"what are your services",
"what services do you ofer",
"what you do",
"your service",
"tell me service"
],
"slots": []
},
{
"name": "GetTechnologiesIntent",
"samples": [
"list me your technologies",
"give me your technologies",
"what you know"
],
"slots": []
}
],
"invocationName": "saburly"
}
}

15
backend/config.js Normal file
View File

@@ -0,0 +1,15 @@
var config = {};
config.dbURL = 'mongodb://localhost:27017/tellall';
config.PORT = 5000;
config.TOKEN = 'Atza|IwEBIBe6gDqrrowEEav6N-_6s4NztYeP3oG8PGWmu8ZiZw6lbOh3wNla3TK6pY-VEpT1d8an-dVf_n3kXJzVFsNo_4xBfZyFHGoCTDTFjs3yBRul4PVdBOhwwiH3-sgRLcUofZbe2oE06GmTcbfYtaStfXpQI5dfpldfnsJg_CvhSA6AHb_snJT3F6lyXzbV076d_3cYUMJxFldJGnYcviNHHxjjmuQTD06hhGzCbAxxe9eBmkuopRsNfyedLT2UlKP_ublah9CUGA3AdIX_3Iuke82jMwGnNl9gv7pbaDNEjAbj7IQSl3B08uuREtJq-oTBOjALNXRvFxTJmQjZwXNf9eHC7fSHJDdEPdZQU0AcffRQObAyAkUuL6Jv39OHzhb3Q64-zzoyODqnJyLP5SQZ2JVF53Kc_cTBqjIc9pXljqe7yEVk6JDs7q1zKbBibx_AQm57TO79IzWyLBzBMlYL5HdTsqEfRzLeDw2tws-hGMgkx2HWfdbYnmf5Qb4SyIhzvmmdfPLg3MVKTxjIBu1rx0xf3n0PLZP1EO6jsJPoMRPg77Gm4oit5Zp6s37ek3A3Vxh-ntoASpkrkxGTG9kVtRNt';
config.REFRESH_TOKEN = 'Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH';
config.TOKEN_EXPIRES_IN = 1515100500;
config.SKILL_ID = 'amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae';
config.CLIENT_ID = 'amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7';
config.CLIENT_SECRET = '6dea8125cecd049d3c4cff7bb5bdfd3ff17bc6fed246c4c8f6b519d9ed08d0b3';
module.exports=config;

177
backend/express.js Normal file
View File

@@ -0,0 +1,177 @@
var amazonHelper = require('./helpers/amazon');
var databaseHelper = require('./helpers/database');
const config = require('./config');
require('isomorphic-fetch');
var express = require('express');
var alexa = require('alexa-app');
var bodyParser = require('body-parser');
var MongoClient = require ('mongodb').MongoClient;
var ObjectID = require ('mongodb').ObjectID;
const router = express.Router ();
var skillID = 'amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3';
var skillDbID = '5a232fb86ce046c749739455'; //_id in database
var app = express();
// 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
alexaApp.express({
expressApp: app,
// verifies requests come from amazon alexa. Must be enabled for production.
// You can disable this if you're running a dev environment and want to POST
// things to test behavior. enabled by default.
checkCert: false,
// sets up a GET route when set to true. This is handy for testing in
// development, but not recommended for production. disabled by default
debug: true
});
// now POST calls to /test in express will be handled by the app.request() function
// from here on you can setup any other express routes or middlewares as normal
app.set("view engine", "ejs");
alexaApp.launch(function(request, response) {
console.log("Alexa launch");
console.log(request);
const skill = db.collection('skill_list').findOne({_id: ObjectID(skillDbID)}, (err,result)=>{
if (err){
response.say("I could not find desired skill")
}else{
response.say(skill.invocationAnswer);
}
});
});
var findElementByAttr = function (data, attr, val){
for(var i = 0; i < data.length; i ++) {
if(data[i][attr] === val) {
return i;
}
}
return -1;
}
alexaApp.request = (jsonRequest) => {
const alexaRequest = new alexa.request(jsonRequest);
if (alexaRequest.type() === "IntentRequest") {
const skill = db.collection('skill_list').findOne({
_id: skillDbID
});
if (skill) {
let intentId = findElementByAttr(skill.intents, 'intentName', alexaRequest.data.request.intent.name);
const response = new alexa.response(alexaRequest.getSession());
if (intentId !== -1){
return response.say(skill.intents[intentId].answer);
}else{
return response.say('Sorry, I could not find desired intent');
}
}
}
};
router.get ('/getSkill/:id', async (req, res, next) => {
try {
const id = req.params.id;
if (id.length !== 24){
res.json([]);
throw("error");
}
db.collection ('skill_list').find({_id: ObjectID(id)}).toArray((err,result)=>{
if (err){
throw err;
res.json([]);
}else{
res.json(result);
}
});
} catch (e) {
console.log ('error:', e);
next (e);
}
});
router.get ('/deleteSkill/:skillID', async (req, res, next) => {
try {
let id = req.params.id;
let result = db.collection('skill_list').remove({_id: ObjectID(id)},(err,result)=>{
if (err) throw err;
res.json(result);
});
} catch (e) {
console.log ('error:', e);
next (e);
}
});
router.post ('/updateSkill/:id', async (req, res, next) => {
let id = req.params.id;
let skill = req.body;
delete skill._id;
if (id !== '-1'){
amazonHelper.updateSkill(skill,db).then(amazonResult=>{
if (amazonResult===200 || amazonResult===202){
//Skill uploaded, it's ok to update database
let result = db.collection('skill_list').update({_id: ObjectID(id)}, skill,{upsert:true}, (err, result)=>{
if (JSON.parse(result).nModified===1){
//database update ok
res.json({result:0, message:'ok'});
}else{
res.json({result:-1, message:'ok'});
}
});
}else{
res.json({result:-1, message:'Amazon result ' + amazonResult});
}
}).catch(e=>{
//skill upload went wrong, don't update database, send error
res.json({result:-1, message:e});
});
}else{
//no new skills for now
}
});
app.use (function (req, res, next) {
res.header ('Access-Control-Allow-Origin', '*');
res.header (
'Access-Control-Allow-Headers',
'Origin, Content-Type'
);
res.header ('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header ('Access-Control-Allow-Credentials', 'true');
next ();
});
app.use (bodyParser.json ());
app.use ('/', router);
MongoClient.connect (config.dbURL).then (database => {
db = database;
db.collection ('intent_list');
databaseHelper.initModule(db);
app.listen (config.PORT, () =>{
console.log ('Express server running on port ' + config.PORT);
databaseHelper.loadTokens();
});
}).catch(e=>{
console.log("error : " + e);
});

118
backend/helpers/amazon.js Normal file
View File

@@ -0,0 +1,118 @@
require('isomorphic-fetch');
const config = require('../config');
var request = require("request");
var token = require('./token');
var getBuildStatus = function(skillID){
try{
fetch(`https://api.amazonalexa.com/v0/skills/${skillID}/interactionModel/locales/en-US/status`, {
method: 'GET',
headers: {
Authorization: TOKEN
},
}).then(l=>l.text()).then(result=>{
return result;
});
}catch(e){
console.log("err : " + e);
}
}
var refreshToken = function(db){
return new Promise((resolve, reject)=>{
var options = { method: 'POST',
url: 'https://api.amazon.com/auth/o2/token',
headers:
{ 'cache-control': 'no-cache',
'content-type': 'application/x-www-form-urlencoded' },
form:
{ grant_type: 'refresh_token',
refresh_token: config.REFRESH_TOKEN,
client_id: config.CLIENT_ID,
client_secret: config.CLIENT_SECRET } };
request(options, function (error, response, body) {
if (error) reject(error);
parsedResponse = JSON.parse(body);
if (parsedResponse.refresh_token){
try{
token.updateTokens(parsedResponse.refresh_token, parsedResponse.access_token, parsedResponse.expires_in,db);
resolve();
}catch(e){
reject(e);
}
}
});
});
}
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});
});
result.interactionModel = {};
result.interactionModel.languageModel = {
invocationName: skill.invocationName,
intents: allIntents
};
return JSON.stringify(result);
}
var uploadSkill = function(skill){
return fetch(`https://api.amazonalexa.com/v0/skills/${skill.skillID}/interactionModel/locales/en-US`, {
method: 'POST',
headers: {
Authorization: config.TOKEN
},
body: generateInteractionModel(skill)
});
}
module.exports = {
updateSkill: function(skill, db){
return new Promise((resolve,reject)=>{
if (new Date() / 1000 > config.TOKEN_EXPIRES_IN){
refreshToken(db).then(()=>{
uploadSkill(skill).then(response=>{
resolve(response.status);
});
}).catch(e=>{
reject(e);
});
}else{
uploadSkill(skill).then(response=>{
resolve(response.status);
}).catch(e=>{
reject(e);
})
}
});
}
};

View File

@@ -0,0 +1,27 @@
const config = require('../config');
var db = null;
module.exports = {
initModule : function(databaseObject){
db=databaseObject;
},
loadTokens : function(){
db.collection('token_list').findOne().then(tokens=>{
if (tokens !== null){
config.TOKEN = tokens.access_token;
config.REFRESH_TOKEN = tokens.refresh_token;
config.TOKEN_EXPIRES_IN = tokens.expires_in;
}else{
//Cannot continue without tokens
console.log("Cannot continue without tokens in database");
process.exit(-1);
}
}).catch(e=>{
console.log("Error loading tokens ! Cannot continue without tokens in database");
process.exit(-1);
})
}
}

19
backend/helpers/token.js Normal file
View File

@@ -0,0 +1,19 @@
const config = require('../config');
var ObjectID = require ('mongodb').ObjectID;
module.exports = {
updateTokens : function (refresh_token, access_token, expires_in, db){
let newTokenDocument = {
id:1,
refresh_token:refresh_token,
access_token: access_token,
expires_in: (new Date() / 1000) + expires_in
}
let result = db.collection('token_list').update({id:1}, newTokenDocument,{upsert:true}, (err, result)=>{
if (err) throw new Error(err);
config.REFRESH_TOKEN = refresh_token;
config.TOKEN = access_token;
config.TOKEN_EXPIRES_IN = newTokenDocument.expires_in;
});
}
};

View File

@@ -4,10 +4,13 @@
"description": "",
"main": "test.js",
"dependencies": {
"alexa-app": "4.2.0",
"body-parser": "^1.13.1",
"ejs": "^2.3.1",
"ejs": "^2.5.7",
"express": "^4.13.0",
"alexa-app": "4.2.0"
"isomorphic-fetch": "^2.2.1",
"mongodb": "^2.2.33",
"request": "^2.83.0"
},
"author": "Matt Kruse <github@mattkruse.com> (http://mattkruse.com/)",
"license": "MIT"

View File

@@ -1,43 +0,0 @@
var express = require("express");
var alexa = require("alexa-app");
var PORT = process.env.port || 5000;
var app = express();
// ALWAYS setup the alexa app and attach it to express before anything else.
var alexaApp = new alexa.app("step1");
alexaApp.express({
expressApp: app,
// verifies requests come from amazon alexa. Must be enabled for production.
// You can disable this if you're running a dev environment and want to POST
// things to test behavior. enabled by default.
checkCert: false,
// sets up a GET route when set to true. This is handy for testing in
// development, but not recommended for production. disabled by default
debug: true
});
// now POST calls to /test in express will be handled by the app.request() function
// from here on you can setup any other express routes or middlewares as normal
app.set("view engine", "ejs");
alexaApp.launch(function(request, response) {
response.say("You launched Saburly app!");
});
alexaApp.intent("GetProcessIntent", {
"utterances": [
"tell me about projects", "say something about your project", "what are your projects"
]
},
function(request, response) {
response.say("We collaborate closely with our clients at each step of the developmentprocess. From designing the UX to developing the front-end andarchitecting the back-end.");
}
);
app.listen(PORT);
console.log("Listening on port " + PORT + ", try http://localhost:" + PORT + "/step1");

View File

@@ -1,14 +0,0 @@
var alexa = require("alexa-app");
var find = require("find-my-iphone");
var app = new alexa.app();
app.launch(function(request, response) {
find("me@icloud.com", "mypassword", "iPhone", function() {
response.say("OK").send();
});
// because this is an async handler
return false;
});
// connect to lambda
exports.handler = app.lambda();

View File

@@ -1,116 +0,0 @@
var template = {};
// LaunchRequest template
template.launch = {
"version": "1.0",
"session": {
"new": true,
"sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
"attributes": {},
"application": {
"applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3"
},
"user": {
"userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
}
},
"request": {
"type": "LaunchRequest",
"requestId": "amzn1.echo-api.request.9cdaa4db-f20e-4c58-8d01-c75322d6c423"
}
};
// IntentRequest template
template.intent = {
"version": "1.0",
"session": {
"new": false,
"sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
"attributes": {},
"application": {
"applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3"
},
"user": {
"userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
}
},
"request": {
"type": "IntentRequest",
"requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d",
"intent": {
"name": "sampleIntent",
"slots": {
"NAME": {
"name": "NAME",
"value": "Matt"
}
}
}
}
};
// errorIntent template
template.errorIntent = {
"version": "1.0",
"session": {
"new": false,
"sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
"attributes": {},
"application": {
"applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3"
},
"user": {
"userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
}
},
"request": {
"type": "IntentRequest",
"requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d",
"intent": {
"name": "errorIntent",
"slots": {}
}
}
};
// missingIntent template
template.missingIntent = {
"version": "1.0",
"session": {
"new": false,
"sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
"attributes": {},
"application": {
"applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3"
},
"user": {
"userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
}
},
"request": {
"type": "IntentRequest",
"requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d",
"intent": {
"name": "missingIntent",
"slots": {}
}
}
};
// SessionEndedRequest template
template.session_end = {
"version": "1.0",
"session": {
"new": false,
"sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
"attributes": {},
"application": {
"applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3"
},
"user": {
"userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
}
},
"request": {
"type": "SessionEndedRequest",
"requestId": "amzn1.echo-api.request.d8c37cd6-0e1c-458e-8877-5bb4160bf1e1",
"reason": "USER_INITIATED"
}
};
module.exports = template;

79
test.js
View File

@@ -1,79 +0,0 @@
var alexa = require("alexa-app");
var template = require("./template.js");
var app = new alexa.app("test");
app.dictionary = {
"names": ["Bob", "Jack", "Matt", "Mary", "Jane", "Bill"]
};
app.launch(function(request, response) {
response.say("App launched!");
});
app.intent("sampleIntent", {
"slots": { "NAME": "LITERAL", "AGE": "NUMBER" },
"utterances": ["my {name is|name's} {names|NAME} and {I am|I'm} {1-100|AGE}{ years old|}"]
},
function(request, response) {
setTimeout(function() {
response.say("After timeout!").say(" test ").reprompt("Reprompt");
response.send();
}, 1000);
// We are async!
return false;
}
);
app.intent("errorIntent", function(request, response) {
response.say(someVariableThatDoesntExist);
});
// output the schema
console.log("\n\nSCHEMA:\n\n" + app.schema() + "\n\n");
// output sample utterances
console.log("\n\nUTTERANCES:\n\n" + app.utterances() + "\n\n");
// test pre() and post() functions
app.pre = function(request, response, type) {
response.say("This part of the output is from pre(). ");
};
app.post = function(request, response, type, exception) {
if (exception) {
response.clear().say("An error occured: " + exception).send();
}
};
// error example
app.request(template.errorIntent)
.then(function(response) {
console.log(JSON.stringify(response, null, 3));
});
// async example
app.request(template.intent)
.then(function(response) {
console.log(JSON.stringify(response, null, 3));
});
// synchronous example
app.request(template.launch)
.then(function(response) {
console.log(JSON.stringify(response, null, 3));
});
// error example
app.messages.NO_INTENT_FOUND = "Why you called dat intent? I don't know bout dat";
app.request(template.missingIntent)
.then(function(response) {
console.log(JSON.stringify(response, null, 3));
});
// error handler example
app.error = function(e, request, response) {
response.say("I captured the exception! It was: " + e.message);
};
app.request(template.errorIntent)
.then(function(response) {
console.log(JSON.stringify(response, null, 3));
});

23
web/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
src/**/*.css

62
web/README.md Normal file
View File

@@ -0,0 +1,62 @@
To obtain new Auth Code :
https://www.amazon.com/ap/oa?client_id=amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test&response_type=code&redirect_uri=https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO
Response URL (Decoded) :
https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO?code=ANCgZUfEFdlRRkpSNFuA&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test
Code : ANCgZUfEFdlRRkpSNFuA
=======================================
Now to get Access Token :
Send a POST request to https://api.amazon.com/auth/o2/token with the following parameters
- HTTP Header Parameters
Content-Type: application/x-www-form-urlencoded
- HTTP Body Parameters
grant_type: authorization_code
code: The authorization code that was returned in the response.
client_id: The products client ID. To access this information, navigate to Amazons Developer Console. After youve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product.
client_secret: The products client secret. To access this information, navigate to Amazons Developer Console. After youve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product.
redirect_uri: One of the return URIs that you added to your apps security profile when signing up.
Response :
{
"access_token": "Atza|IwEBIBe6gDqrrowEEav6N-_6s4NztYeP3oG8PGWmu8ZiZw6lbOh3wNla3TK6pY-VEpT1d8an-dVf_n3kXJzVFsNo_4xBfZyFHGoCTDTFjs3yBRul4PVdBOhwwiH3-sgRLcUofZbe2oE06GmTcbfYtaStfXpQI5dfpldfnsJg_CvhSA6AHb_snJT3F6lyXzbV076d_3cYUMJxFldJGnYcviNHHxjjmuQTD06hhGzCbAxxe9eBmkuopRsNfyedLT2UlKP_ublah9CUGA3AdIX_3Iuke82jMwGnNl9gv7pbaDNEjAbj7IQSl3B08uuREtJq-oTBOjALNXRvFxTJmQjZwXNf9eHC7fSHJDdEPdZQU0AcffRQObAyAkUuL6Jv39OHzhb3Q64-zzoyODqnJyLP5SQZ2JVF53Kc_cTBqjIc9pXljqe7yEVk6JDs7q1zKbBibx_AQm57TO79IzWyLBzBMlYL5HdTsqEfRzLeDw2tws-hGMgkx2HWfdbYnmf5Qb4SyIhzvmmdfPLg3MVKTxjIBu1rx0xf3n0PLZP1EO6jsJPoMRPg77Gm4oit5Zp6s37ek3A3Vxh-ntoASpkrkxGTG9kVtRNt",
"refresh_token": "Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH",
"token_type": "bearer",
"expires_in": 3600
}
================================
Now to get new Access token using refresh token :
Send a POST request to https://api.amazon.com/auth/o2/token with the following parameters:
- HTTP Header Parameters
Content-Type: application/x-www-form-urlencoded
- HTTP Body Parameters
grant_type: refresh_token
refresh_token: The URL-encoded refresh token returned with the last request for a new access token.
client_id: To access this information, navigate to Amazons Developer Console. After youve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product.
client_secret: The websites client secret. To access this information, navigate to Amazons Developer Console. After youve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product.
Response :
{
"access_token": "Atza|IwEBILtBe3hrHovrMx7Oivng-RB2EKzvCm_epXJE2HXPMQzXTFqK10Zrqt-Z8paeRoLQBqbLCmqWvcr5RTNgw9qjtfzOTsOrXC1VKqKmxpqHTrJyn2TLGsCzFjBDfADNjCyufWTf2ZlsSzjxW2GiqCHlwoPSd9pFrLavtRThrm1J-5KvnFrj-yD-tYTSwrgX5W5p2SrjQxoE3aP5b96z6p8GvCL9lM1pddafAxkHb22A3IzR-pYGmEijb4ksRuaIf4WCNwssWV6GBIB2oJA5CU-Dtd2mOZZ5-dYpSSeCHyGumTYecTxxMVSdiVjCqB8WT6AtvvutWFQQoldHjJmIwBsTZP-iQcl-UyajOZJ03GqRUym5Hp-49uByzVG-MfR_Z5qVmYjjsLQEOLCY9kPVnmRGnOTj6YPjrHXibd6P8TQOMh4VTcgFpg-afKKABP6EeDwok1t2ivuYh5OJju-B1A6gzhMi4vQJYKq107e0QMYBBhrf_OqCgMbfnQZ8j40qocVGID5YWv8uk5wKyI61LrbzrTltmzxzNemzqbSBzwAlfNS6GW-jVjg8svsi1lb_EVRbhyOoWJWX3mEd-5GDYyUcyInleiAR0aIHVP94pZxqdiCamA",
"refresh_token": "Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH",
"token_type": "bearer",
"expires_in": 3600
}

29
web/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "web",
"version": "0.1.0",
"private": true,
"dependencies": {
"keymaster": "^1.6.2",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-md": "^1.2.8",
"react-popup": "^0.9.1",
"react-scripts": "1.0.17",
"webfontloader": "^1.6.28"
},
"scripts": {
"build-css": "node-sass-chokidar --include-path ./node_modules src/ -o src/",
"watch-css-mine": "npm run build-css && npm run build-css --watch --recursive",
"watch-css": "nodemon -e scss -x \"npm run watch-css-mine\"",
"start-js": "react-scripts start",
"start": "npm-run-all -p watch-css start-js",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {
"node-sass": "^4.7.2",
"nodemon": "^1.12.1",
"npm-run-all": "^4.1.2"
}
}

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

40
web/public/index.html Normal file
View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

15
web/public/manifest.json Normal file
View File

@@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

178
web/src/App.js Normal file
View File

@@ -0,0 +1,178 @@
import React, { Component } from 'react';
import './css/App.css';
import './css/popup.css';
import IntentList from './components/IntentList';
import IntentDetails from './components/IntentDetails';
import LaunchRequest from './components/LaunchRequest';
import Popup from 'react-popup';
import {getSkill, updateSkill} from './lib/api'
class App extends Component {
constructor(props){
super(props);
this.state={_id:'5a232fb86ce046c749739455',
skillID:'',
skillName:'',
invocationName:'Saburly',
invocationAnswer:'We are saburly',
allIntents:[],
selectedIntent: {intentName:'',questions:[''],answer:''},
selectedIndex:-1,
launchRequest:false};
getSkill(this.state._id).then(l=> l.text()).then(result=>{
let jResult = JSON.parse(result)[0];
if (jResult===undefined) return;
this.setState({ skillID:jResult.skillID,skillName:jResult.skillName, invocationName: jResult.invocationName,
invocationAnswer: jResult.invocationAnswer,
allIntents: jResult.intents})
})
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);
}
render() {
if(this.state.launchRequest){
return (
<div className="App">
<Popup/>
<div className="App-header">
<h1> Tell All </h1>
</div>
<IntentList allIntents={this.state.allIntents}
onLaunchRequestClick={this.handleLaunchRequestClick}
onIntentClick={this.handleIntentClick}
onAddIntentClick={this.handleAddIntentClick}
selectedIndex={this.state.selectedIndex}>
</IntentList>
<LaunchRequest invocationName={this.state.invocationName}
invocationAnswer={this.state.invocationAnswer}
onSaveClick={this.handleSaveLaunchRequestClick}> </LaunchRequest>
</div>
);
}else{
return (
<div className="App">
<Popup/>
<div className="App-header">
<h1> Tell All </h1>
</div>
<IntentList allIntents={this.state.allIntents}
onLaunchRequestClick={this.handleLaunchRequestClick}
onIntentClick={this.handleIntentClick}
onAddIntentClick={this.handleAddIntentClick}
selectedIndex={this.state.selectedIndex}>
</IntentList>
<IntentDetails selectedIntent={this.state.selectedIntent}
onDeleteIntentClick={this.handleDeleteIntentClick}
onSaveIntentClick={this.handleSaveIntentClick}>
</IntentDetails>
</div>
);
}
}
createSkill(intents, name, answer){
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
};
}
handleIntentClick(selectedIntent, index){
this.setState({selectedIntent:selectedIntent, selectedIndex: index, launchRequest:false});
}
handleLaunchRequestClick(){
this.setState({selectedIndex: -2, launchRequest:true});
}
handleSaveLaunchRequestClick(name, answer){
this.setState({invocationName: name, invocationAnswer: answer});
console.log("handleSaveLaunchRequest");
updateSkill(this.createSkill(this.state.allIntents,name,answer)).then(l=>l.text()).then(result=>{
let jResult = JSON.parse(result);
if (jResult.result !== 0){
Popup.alert('Model was not saved. Please try again');
}else{
Popup.alert('Saved');
}
console.log(jResult.message);
}).catch(e=>{
console.log("Error :" + e);
Popup.alert('Model was not saved. Please try again');
});
}
handleDeleteIntentClick(selectedIntent){
let id = this.state.allIntents.indexOf(selectedIntent);
if (id!==-1){
try{
//I don't like this, state in database is different than component state, for some time
//TODO : move database operation in componentWillUpdate or componentDidUpdate
let newAllIntents = this.state.allIntents;
newAllIntents.splice(id,1);
this.setState({allIntents: newAllIntents, selectedIntent: {intentName:'', questions:[''],answer:''}});
updateSkill(this.createSkill(newAllIntents)).then(l=>l.text()).then(result=>{
let jResult = JSON.parse(result);
if (jResult.result !== 0){
Popup.alert('Model was not saved. Please try again');
}else{
Popup.alert('Saved');
}
console.log(jResult.message);
}).catch(e=>{
console.log("error : " + e);
Popup.alert('Model was not saved. Please try again');
});
}catch(e){
console.log("error : " + e);
}
}
}
handleSaveIntentClick(selectedIntent){
let newAllIntents = this.state.allIntents;
if (this.state.selectedIndex === -1){
//new intent
newAllIntents.push(selectedIntent);
this.setState({allIntents: newAllIntents, selectedIntent: selectedIntent, selectedIndex: newAllIntents.length-1});
}else{
newAllIntents[this.state.selectedIndex] = selectedIntent;
this.setState({allIntents: newAllIntents, selectedIntent: selectedIntent});
}
updateSkill(this.createSkill(newAllIntents)).then(l=>l.text()).then(result=>{
let jResult = JSON.parse(result);
if (jResult.result !== 0){
Popup.alert('Model was not saved. Please try again');
}else{
Popup.alert('Saved');
}
console.log(jResult.message);
}).catch(e=>{
Popup.alert('Model was not saved. Please try again');
console.log("error : " + e);
});
}
handleAddIntentClick(){
this.setState({allIntents: this.state.allIntents, selectedIndex: -1,launchRequest:false,selectedIntent: {intentName:'',questions:[''], answer:''}});
}
}
export default App;

8
web/src/App.test.js Normal file
View File

@@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
});

View File

@@ -0,0 +1,119 @@
import React, { Component } from 'react';
import {Button, SVGIcon, TextField} from 'react-md';
import '../css/Intent.css'
import {QUESTION_MAX_LENGTH, ANSWER_MAX_LENGTH, INTENT_NAME_MAX_LENGTH} from '../config';
class IntentDetails extends Component {
constructor(props){
super(props);
this.state= {intent: props.selectedIntent};
this.addQuestion = this.addQuestion.bind(this);
this.deleteQuestion = this.deleteQuestion.bind(this);
this.handleQuestionEdit = this.handleQuestionEdit.bind(this);
this.handleAnswerEdit = this.handleAnswerEdit.bind(this);
this.handleIntentNameEdit = this.handleIntentNameEdit.bind(this);
}
componentWillReceiveProps(props){
this.setState({intent: props.selectedIntent});
}
render() {
return (
<div className="IntentDetails">
<div className="QuestionBox">
<TextField
id="intent name"
lineDirection="center"
placeholder="Intent name"
label="Intent name"
className="md-cell md-cell--bottom"
style={{width:'60%'}}
onChange={this.handleIntentNameEdit}
maxLength={INTENT_NAME_MAX_LENGTH}
value={this.state.intent.intentName} />
</div>
<h5 style={{marginTop:'20px', marginLeft: '30px', float: 'left'}}>Questions</h5>
{
this.state.intent.questions.map((question, index)=>{
return (
<div key={index} className="QuestionBox">
<TextField
id="intent question"
lineDirection="center"
placeholder="Question"
className="md-cell md-cell--bottom"
style={{width:'60%'}}
maxLength={QUESTION_MAX_LENGTH}
rightIcon={<SVGIcon onClick={()=>{this.deleteQuestion(question)}}> <path fill="#000000" d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"/> </SVGIcon>}
onChange={(e)=>{this.handleQuestionEdit(e,index)}}
value={question}/>
</div>
);
})
}
<br></br>
{
<div className="QuestionBox">
<TextField
id="intent answer"
lineDirection="center"
label="Answer"
placeholder="Answer"
style={{width:'60%'}}
maxLength={ANSWER_MAX_LENGTH}
className="md-cell md-cell--bottom"
onChange={this.handleAnswerEdit}
value={this.state.intent.answer}/>
</div>
}
<br></br>
<br></br>
<Button style={{float:'left', marginLeft: '25px'}} flat primary onClick={()=>{this.props.onDeleteIntentClick(this.state.intent)}}>Delete intent</Button>
<Button style={{float:'left', marginLeft: '22%'}} flat primary swapTheming onClick={this.addQuestion}>Add question</Button>
<Button style={{float:'left', marginLeft: '20px'}} flat primary swapTheming onClick={()=>{this.props.onSaveIntentClick(this.state.intent)}}>Save</Button>
</div>
);
}
addQuestion(){
let newIntent = this.state.intent;
newIntent.questions.push('');
this.setState({intent: newIntent});
}
deleteQuestion(question){
let newIntent = this.state.intent;
let removeId = newIntent.questions.indexOf(question);
if (removeId !== -1)
newIntent.questions.splice(removeId,1);
this.setState({intent: newIntent});
}
handleQuestionEdit(e,index){
if (e.length === QUESTION_MAX_LENGTH) return;
let newIntent = this.state.intent;
newIntent.questions[index] = e;
this.setState({intent: newIntent});
}
handleAnswerEdit(e){
if (e.length === ANSWER_MAX_LENGTH) return;
let newIntent = this.state.intent;
newIntent.answer = e;
this.setState({intent: newIntent});
}
handleIntentNameEdit(e){
if (e.length === INTENT_NAME_MAX_LENGTH) return;
//e.replace(/\s/g,'');
let newIntent = this.state.intent;
newIntent.intentName = e;
this.setState({intent: newIntent});
}
}
export default IntentDetails;

View File

@@ -0,0 +1,34 @@
import React, { Component } from 'react';
import {Button} from 'react-md';
import '../css/Intent.css'
import {INTENT_TITLE_MAX_LENGTH, INTENT_TITLE_TOOLTIP_DELAY} from '../config'
class IntentItem extends Component {
constructor(props){
super(props);
this.state={intent: props.intent, index: props.index, onClick: props.onClick};
}
render() {
let buttonTitle = this.state.intent.intentName;
if (buttonTitle.length > INTENT_TITLE_MAX_LENGTH){
buttonTitle = this.state.intent.intentName.substr(0,INTENT_TITLE_MAX_LENGTH-1) + '. . .';
}
return (
<div>
<Button className={this.props.selectedIndex===this.state.index ? 'IntentItem-selected' : 'IntentItem'}
onClick={()=>{this.state.onClick(this.state.intent,this.state.index)}}
flat
tooltipDelay={INTENT_TITLE_TOOLTIP_DELAY}
tooltipLabel={this.state.intent.intentName.length>INTENT_TITLE_MAX_LENGTH ? this.state.intent.questions[0] : ''}>
{this.state.index+1}. {buttonTitle}
</Button>
<br></br>
</div>
);
}
}
export default IntentItem;

View File

@@ -0,0 +1,41 @@
import React, { Component } from 'react';
import {Button} from 'react-md';
import IntentItem from './IntentItem';
import '../css/Intent.css'
class IntentList extends Component {
constructor (props){
super(props);
this.state = {intents: props.allIntents, selectedIndex:props.selectedIndex, onIntentClick:props.onIntentClick};
}
componentWillReceiveProps(props){
this.setState({intents: props.allIntents, selectedIndex: props.selectedIndex, onIntentClick: props.onIntentClick});
}
render() {
return (
<div className="IntentList">
<Button className={this.props.selectedIndex===-2 ? "LaunchRequest-selected" : "LaunchRequest"} flat primary
onClick={this.props.onLaunchRequestClick}>Launch request</Button>
<div className="IntentList-title">
<h3>Intents</h3>
</div>
{
this.state.intents.map((intent,index)=>{
return <IntentItem
key={index} intent={intent} index={index}
selectedIndex={this.props.selectedIndex}
onClick={this.state.onIntentClick}>
</IntentItem>
})
}
<br></br>
<Button className={"AddIntent"} flat primary swapTheming onClick={this.props.onAddIntentClick}>Add intent</Button>
</div>
);
}
}
export default IntentList;

View File

@@ -0,0 +1,64 @@
import React, { Component } from 'react';
import {Button, TextField} from 'react-md';
import '../css/Intent.css'
import { INVOCATION_NAME_MAX_LENGTH, INVOCATION_ANSWER_MAX_LENGTH } from '../config';
class LaunchRequest extends Component {
constructor(props){
super(props);
this.state = {invocationName: props.invocationName, invocationAnswer: props.invocationAnswer};
this.handleNameEdit = this.handleNameEdit.bind(this);
this.handleAnswerEdit = this.handleAnswerEdit.bind(this);
}
componentWillReceiveProps(props){
this.setState({invocationName: props.invocationName, invocationAnswer: props.invocationAnswer});
}
render() {
return (
<div className="LaunchRequestBox">
<h5 style={{textAlign:'left', marginTop: '30px', marginLeft: '20px'}}> Invocation name customers use to activate the skill. For example "Open Saburly" or "Talk to Saburly" </h5>
<TextField
id="invocation name"
lineDirection="center"
placeholder="Saburly"
label="Invocation name"
className="md-cell md-cell--bottom"
style={{width:'60%', marginLeft: '20px'}}
maxLength={INVOCATION_NAME_MAX_LENGTH}
onChange={this.handleNameEdit}
value={this.state.invocationName}/>
<br></br>
<h5 style={{textAlign:'left', marginLeft: '20px', marginTop: '30px'}}>Answer customers get from Alexa when they activate the skill.</h5>
<TextField
id="invocation answer"
lineDirection="center"
placeholder="We are Saburly, ask us something about us"
label="Answer"
className="md-cell md-cell--bottom"
style={{width:'60%', marginLeft: '20px'}}
maxLength={INVOCATION_ANSWER_MAX_LENGTH}
onChange={this.handleAnswerEdit}
value={this.state.invocationAnswer}/>
<br></br>
<br></br>
<Button style={{float:'right', marginRight: '20px'}} flat primary swapTheming onClick={()=>{this.props.onSaveClick(this.state.invocationName, this.state.invocationAnswer)}}>Save</Button>
</div>
);
}
handleNameEdit(e){
if (e.length === INVOCATION_NAME_MAX_LENGTH) return;
this.setState({invocationName: e});
}
handleAnswerEdit(e){
if (e.length === INVOCATION_ANSWER_MAX_LENGTH) return;
this.setState({invocationAnswer: e});
}
}
export default LaunchRequest;

11
web/src/config.js Normal file
View File

@@ -0,0 +1,11 @@
export const BASE_URL = 'localhost:5000';
export const INTENT_NAME_MAX_LENGTH = 30;
export const QUESTION_MAX_LENGTH = 150;
export const ANSWER_MAX_LENGTH = 150;
export const INTENT_TITLE_MAX_LENGTH = 20;
export const INTENT_TITLE_TOOLTIP_DELAY = 700;
export const INVOCATION_NAME_MAX_LENGTH = 15;
export const INVOCATION_ANSWER_MAX_LENGTH = 100;

13
web/src/css/App.css Normal file
View File

@@ -0,0 +1,13 @@
.App {
text-align: center; }
.App-header {
background-color: white;
height: 80px;
padding: 20px; }
.App-title {
font-size: 1.5em; }
.App-intro {
font-size: large; }

17
web/src/css/App.scss Normal file
View File

@@ -0,0 +1,17 @@
.App {
text-align: center;
}
.App-header {
background-color:white;
height: 80px;
padding: 20px;
}
.App-title {
font-size: 1.5em;
}
.App-intro {
font-size: large;
}

71
web/src/css/Intent.css Normal file
View File

@@ -0,0 +1,71 @@
/*IntentList and IntentItem CSS*/
.IntentList {
width: 30%;
min-height: 9999px;
height: 9999px;
float: left;
background-color: #eff0f0; }
.IntentList-title {
font-size: 1.5em;
height: 70px;
padding: 20px;
text-align: left;
background-color: #eff0f0; }
.LaunchRequest {
text-align: left;
width: 100%;
height: 50px;
background-color: #d8d8d8; }
.LaunchRequest-selected {
text-align: left;
width: 100%;
height: 50px;
background-color: #f5f5f5; }
.AddIntent {
float: right;
margin: 12px; }
.IntentItem {
margin-top: 2px;
margin-bottom: 2px;
height: 50px;
width: 100%;
text-align: left;
background-color: #d8d8d8; }
.IntentItem-selected {
margin-top: 2px;
margin-bottom: 2px;
height: 50px;
width: 100%;
text-align: left;
background-color: #f5f5f5; }
/*IntentDetails CSS*/
.IntentDetails {
float: left;
width: 70%;
min-height: 9999px;
height: 9999px;
background-color: #f5f5f5; }
.QuestionBox {
margin: 25px; }
/*LaunchRequest CSS*/
.LaunchRequestBox {
float: left;
width: 70%;
min-height: 9999px;
height: 9999px;
background-color: #f5f5f5; }
.ExplanationText {
float: left;
margin-top: 30px;
margin-left: 20px;
text-align: left; }

86
web/src/css/Intent.scss Normal file
View File

@@ -0,0 +1,86 @@
/*IntentList and IntentItem CSS*/
.IntentList{
width: 30%;
min-height:9999px;
height:9999px;
float:left;
background-color: #eff0f0;
}
.IntentList-title{
font-size: 1.5em;
height: 70px;
padding: 20px;
text-align:left;
background-color: #eff0f0;
}
.LaunchRequest{
text-align: left;
width: 100%;
height: 50px;
background-color: #d8d8d8
}
.LaunchRequest-selected{
text-align: left;
width: 100%;
height: 50px;
background-color: #f5f5f5;
}
.AddIntent{
float: right;
margin: 12px;
}
.IntentItem{
margin-top: 2px;
margin-bottom: 2px;
height: 50px;
width:100%;
text-align:left;
background-color: #d8d8d8;
}
.IntentItem-selected{
margin-top: 2px;
margin-bottom: 2px;
height: 50px;
width:100%;
text-align:left;
background-color: #f5f5f5;
}
/*IntentDetails CSS*/
.IntentDetails{
float: left;
width: 70%;
min-height:9999px;
height:9999px;
background-color: #f5f5f5;
}
.QuestionBox{
margin:25px;
}
/*LaunchRequest CSS*/
.LaunchRequestBox{
float: left;
width:70%;
min-height: 9999px;
height: 9999px;
background-color: #f5f5f5;
}
.ExplanationText{
float: left;
margin-top: 30px;
margin-left: 20px;
text-align: left;
}

View File

@@ -0,0 +1,5 @@
@import 'react-md/src/scss/react-md';
// Any variable overrides. The following just changes the default theme to use teal and purple.
$md-primary-color: $md-teal-500;
$md-secondary-color: $md-purple-a-400;

3627
web/src/css/index.css Normal file

File diff suppressed because it is too large Load Diff

9
web/src/css/index.scss Normal file
View File

@@ -0,0 +1,9 @@
@import 'globals';
@include react-md-everything;
body {
margin: 0;
padding: 0;
}

205
web/src/css/popup.scss Normal file
View File

@@ -0,0 +1,205 @@
.mm-popup {
display: none;
}
.mm-popup--visible {
display: block;
}
.mm-popup__overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
overflow: auto;
background: rgba(0, 0, 0, .1);
}
.mm-popup__close {
position: absolute;
top: 15px;
right: 20px;
padding: 0;
width: 20px;
height: 20px;
cursor: pointer;
outline: none;
text-align: center;
border-radius: 10px;
border: none;
text-indent: -9999px;
background: transparent url('data:image/png;base64,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') no-repeat center center;
background-size: 100%;
margin: 0;
}
.mm-popup__input {
display: block;
width: 100%;
height: 30px;
border-radius: 3px;
background: #f5f5f5;
border: 1px solid #e9ebec;
outline: none;
-moz-box-sizing: border-box !important;
-webkit-box-sizing: border-box !important;
box-sizing: border-box !important;
font-size: 14px;
padding: 0 12px;
color: #808080;
}
.mm-popup__btn {
border-radius: 3px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding: 0 10px;
margin: 0;
line-height: 32px;
height: 32px;
border: 1px solid #666;
text-align: center;
display: inline-block;
font-size: 12px;
font-weight: 400;
color: #333;
background: transparent;
outline: none;
text-decoration: none;
cursor: pointer;
font-family: "Open Sans", sans-serif;
}
.mm-popup__btn--success {
background-color: #27ae60;
border-color: #27ae60;
color: #fff;
}
.mm-popup__btn--danger {
background-color: #c5545c;
border-color: #c5545c;
color: #fff;
}
.mm-popup__box {
width: 350px;
position: fixed;
top: 10%;
left: 50%;
margin-left: -175px;
background: #fff;
box-shadow: 0px 5px 20px 0px rgba(126,137,140,0.20);
border-radius: 5px;
border: 1px solid #B8C8CC;
overflow: hidden;
z-index: 1001;
}
.mm-popup__box__header {
padding: 15px 20px;
background: #EDF5F7;
color: #454B4D;
}
.mm-popup__box__header__title {
margin: 0;
font-size: 16px;
text-align: left;
font-weight: 600;
}
.mm-popup__box__body {
padding: 20px;
line-height: 1.4;
font-size: 14px;
color: #454B4D;
background: #fff;
position: relative;
z-index: 2;
}
.mm-popup__box__body p {
margin: 0 0 5px;
}
.mm-popup__box__footer {
overflow: hidden;
padding: 40px 20px 20px;
}
.mm-popup__box__footer__right-space {
float: right;
}
.mm-popup__box__footer__right-space .mm-popup__btn {
margin-left: 5px;
}
.mm-popup__box__footer__left-space {
float: left;
}
.mm-popup__box__footer__left-space .mm-popup__btn {
margin-right: 5px;
}
.mm-popup__box--popover {
width: 300px;
margin-left: -150px;
}
.mm-popup__box--popover .mm-popup__close {
position: absolute;
top: 5px;
right: 5px;
padding: 0;
width: 20px;
height: 20px;
cursor: pointer;
outline: none;
text-align: center;
border-radius: 10px;
border: none;
text-indent: -9999px;
background: transparent url('data:image/png;base64,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') no-repeat center center;
background-size: 100%;
margin: 0;
z-index: 3;
}
.mm-popup__box--popover .mm-popup__box__body {
padding: 20px;
}
@media (max-width: 420px) {
.mm-popup__box {
width: auto;
left: 10px;
right: 10px;
top: 10px;
margin-left: 0;
}
.mm-popup__box__footer__left-space {
float: none;
}
.mm-popup__box__footer__right-space {
float: none;
}
.mm-popup__box__footer {
padding-top: 30px;
}
.mm-popup__box__footer .mm-popup__btn {
display: block;
width: 100%;
text-align: center;
margin-top: 10px;
}
}

16
web/src/index.js Normal file
View File

@@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './css/index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import WebFontLoader from 'webfontloader';
WebFontLoader.load({
google: {
families: ['Roboto:300,400,500,700', 'Material Icons'],
},
});
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

30
web/src/lib/api.js Normal file
View File

@@ -0,0 +1,30 @@
import fetch from 'isomorphic-fetch';
import {BASE_URL} from '../config';
export const getAllIntents = (id)=>{
let url = `http://${BASE_URL}/intents/${id}`
return fetch(url, {method: 'GET'});
}
export const getSkill = (id)=>{
let url = `http://${BASE_URL}/getSkill/${id}`
return fetch(url, {method: 'GET'});
}
export const deleteSkill = (id)=>{
let url = `http://${BASE_URL}/deleteSkill/${id}`
return fetch(url, {method: 'GET'});
}
export const updateSkill = (skill)=>{
let id = (skill._id) ? skill._id : -1;
let url = `http://${BASE_URL}/updateSkill/${id}`
return fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(skill),
});
}

7
web/src/logo.svg Normal file
View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,108 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}