├── .babelrc ├── .editorconfig ├── .gitignore ├── README.md ├── bin └── start.js ├── package.json ├── server.js ├── src ├── actions │ └── index.js ├── components │ └── Skill │ │ ├── index.js │ │ ├── index.scss │ │ └── requests.json ├── constants │ └── ActionTypes.js ├── containers │ └── App │ │ ├── index.js │ │ └── index.scss ├── index.js └── reducers │ └── index.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets" : ["env", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alexa Skill Test 2 | 3 | Alexa Skill Test provides a live server for local testing of Alexa Skills written in Node.js. Right now, testing skills usually involves going through the [Amazon Alexa Developer Portal](https://developer.amazon.com/alexa). This project makes testing much easier. 4 | 5 | ## Requirements 6 | 7 | * Node/npm 8 | * An Amazon Alexa Skill written in Node.js 9 | ## Install 10 | 11 | It's recommended to install Alexa Skill Test as a global npm package: 12 | 13 | `npm install -g alexa-skill-test` 14 | 15 | After install, the `alexa-skill-test` command will be available to you. 16 | 17 | ## Command 18 | 19 | Alexa Skill Test works off one command: 20 | 21 | `alexa-skill-test [--path] [--interaction-model]` 22 | 23 | `--path` let's you optionally specify a relative path to your skill. `--interaction-model` let's you optionally specify a relative path to your interaction model. 24 | 25 | ## Usage 26 | 27 | Within your terminal, change directory to your valid Amazon skill. Your skill will need a `package.json` and a main script file. Run the following command: 28 | 29 | `alexa-skill-test` 30 | 31 | This starts up a local testing server using your Alexa skill. If you specify a relative path to an interaction model using `--interaction-model`, the app will prefill your skill intents for you. 32 | 33 | In your browser, navigate to `http://localhost:3000`. You should see a simple UI for sending test requests to your skill. 34 | 35 | *Note:* 36 | 37 | In the skill(s) you're testing, you should set your `appId` like so: 38 | 39 | ```js 40 | if ('undefined' === typeof process.env.DEBUG) { 41 | alexa.appId = '...'; 42 | } 43 | ``` 44 | 45 | Setting an `appId` while debugging will cause Lambda to throw an error since there will be a mismatch. Alexa Skill Test will automatically set the `DEBUG` environmental variable. 46 | 47 | ## License 48 | 49 | MIT 50 | -------------------------------------------------------------------------------- /bin/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var ArgumentParser = require('argparse').ArgumentParser; 6 | var nodemon = require('nodemon'); 7 | var path = require('path'); 8 | var fs = require('fs'); 9 | var parser = new ArgumentParser(); 10 | var colors = require('colors'); 11 | 12 | parser.addArgument( 13 | [ '-p', '--path' ], 14 | { 15 | help: 'Path to Alexa skill' 16 | } 17 | ); 18 | 19 | parser.addArgument( 20 | [ '--interaction-model' ], 21 | { 22 | help: 'Path to interaction model' 23 | } 24 | ); 25 | 26 | // Add the port option 27 | parser.addArgument( 28 | ['--port'], 29 | { 30 | help: 'Run on a specific port (default = 3000)' 31 | } 32 | ); 33 | 34 | var args = parser.parseArgs(); 35 | 36 | var currentPath = args.path ? args.path : '.'; 37 | currentPath = path.resolve(currentPath); 38 | 39 | // Set port value 40 | var port = args.port ? args.port : 3000; 41 | 42 | try { 43 | var skillPackageConf = require(currentPath + '/package.json'); 44 | } catch (err) { 45 | console.error('Package.json not found.'.red); 46 | process.exit(1); 47 | } 48 | 49 | if (typeof skillPackageConf.name !== 'string') { 50 | console.error('Package.json requires a name property.'.red); 51 | process.exit(1); 52 | } 53 | 54 | if (!skillPackageConf.main) { 55 | console.error('Main script file not found.'.red); 56 | process.exit(1); 57 | } 58 | 59 | var mainScriptFile = skillPackageConf.main; 60 | 61 | try { 62 | var mainScript = require(currentPath + '/' + mainScriptFile); 63 | } catch (error) { 64 | console.error('Problem with main script file.'.red); 65 | console.log(error); 66 | process.exit(1); 67 | } 68 | 69 | var serverArgs = [ 70 | currentPath + '/' + mainScriptFile, 71 | skillPackageConf.name, '--port ' + port 72 | ]; 73 | 74 | var watchList = [ 75 | __dirname + '/../server.js', 76 | __dirname + '/../package.json', 77 | __dirname + '/../webpack.config.js', 78 | currentPath + '/' 79 | ]; 80 | 81 | if (args.interaction_model) { 82 | watchList.push(path.resolve(args.interaction_model)); 83 | serverArgs.push('--interaction-model ' + path.resolve(args.interaction_model)); 84 | } 85 | 86 | nodemon({ 87 | nodeArgs: (process.env.REMOTE_DEBUG) ? ['--debug'] : [], 88 | script: __dirname + '/../server.js', 89 | args: serverArgs, 90 | watch: watchList, 91 | env: { 92 | 'DEBUG': (process.env.DEBUG) ? process.env.DEBUG : 'skill' 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexa-skill-test", 3 | "version": "1.2.4", 4 | "description": "Test Alexa skills through a local server.", 5 | "main": "bin/start.js", 6 | "engineStrict": true, 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "./node_modules/.bin/webpack" 10 | }, 11 | "bin": { 12 | "alexa-skill-test": "./bin/start.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/tlovett1/alexa-skill-test" 17 | }, 18 | "engines": { 19 | "node": ">=6.7" 20 | }, 21 | "keywords": [ 22 | "alexa", 23 | "alexa skill", 24 | "alexa sdk", 25 | "alexa lambda", 26 | "alexa utterance", 27 | "alexa schema" 28 | ], 29 | "dependencies": { 30 | "add": "^2.0.6", 31 | "argparse": "^1.0.9", 32 | "babel-core": "^6.23.1", 33 | "babel-loader": "^7.1.2", 34 | "babel-preset-env": "^1.6.1", 35 | "babel-preset-react": "^6.23.0", 36 | "body-parser": "^1.17.0", 37 | "bootstrap-sass": "^3.3.7", 38 | "colors": "^1.1.2", 39 | "command-line-args": "^5.0.1", 40 | "connect-livereload": "^0.6.0", 41 | "css-loader": "^0.28.9", 42 | "debug": "^3.1.0", 43 | "express": "^4.16.2", 44 | "immutable": "^3.8.1", 45 | "jquery": "^3.1.1", 46 | "js-beautify": "^1.6.11", 47 | "lambda-local": "^1.4.1", 48 | "lodash": "^4.17.4", 49 | "minimist": "^1.2.0", 50 | "node-sass": "^4.5.0", 51 | "nodemon": "^1.11.0", 52 | "prop-types": "^15.6.0", 53 | "react": "^16.2.0", 54 | "react-dom": "^16.2.0", 55 | "react-redux": "^5.0.3", 56 | "redux": "^3.6.0", 57 | "redux-thunk": "^2.2.0", 58 | "sass-loader": "^6.0.2", 59 | "shortid": "^2.2.8", 60 | "style-loader": "^0.20.1", 61 | "tiny-lr": "^1.0.3", 62 | "webpack": "^3.10.0", 63 | "webpack-dev-middleware": "^2.0.4", 64 | "webpack-dev-server": "^2.4.1", 65 | "yarn": "^1.3.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var path = require('path'); 4 | var lrserver = require('tiny-lr')(); 5 | var pkgConfig = require('./package.json'); 6 | var webpack = require('webpack'); 7 | var webpackMiddleware = require('webpack-dev-middleware'); 8 | var webpackConf = require('./webpack.config.js'); 9 | var webpackCompiller = webpack(webpackConf); 10 | var lambdaLocal = require('lambda-local'); 11 | var bodyParser = require('body-parser'); 12 | var colors = require('colors'); 13 | var debug = require('debug')('skill'); 14 | 15 | var skill = require(process.argv[2]); 16 | 17 | var skillName = process.argv[3]; 18 | 19 | // pull the port number out of the string '--port 3000' 20 | var port = process.argv[4].replace('--port ', ''); 21 | 22 | var interactionModel = false; 23 | if (process.argv[5]) { 24 | var interactionModelPath = process.argv[5].replace('--interaction-model ', ''); 25 | 26 | try { 27 | interactionModel = JSON.stringify(require(interactionModelPath)); 28 | } catch (err) { 29 | // Do nothing 30 | } 31 | } 32 | 33 | var livereloadServerConf = { 34 | port: 35729 35 | }; 36 | 37 | var triggerLiveReloadChanges = function() { 38 | lrserver.changed({ 39 | body: { 40 | files: [webpackConf.output.filename] 41 | } 42 | }); 43 | }; 44 | 45 | lrserver.listen(livereloadServerConf.port, triggerLiveReloadChanges); 46 | 47 | webpackCompiller.plugin('done', triggerLiveReloadChanges); 48 | 49 | app.use(bodyParser.json()); 50 | app.use(bodyParser.urlencoded({ 51 | extended: true 52 | })); 53 | 54 | app.use(webpackMiddleware(webpackCompiller)); 55 | app.use(require('connect-livereload')(livereloadServerConf)); 56 | 57 | app.use(express.static(__dirname + '/public')); 58 | 59 | app.get('/', function(req, res) { 60 | res.end([ 61 | '', 62 | '', 63 | '', 64 | '', 65 | '', 66 | 'Alexa Skills Tester - ' + skillName + '', 67 | '', 68 | '', 69 | '
', 70 | '', 71 | '', 72 | '', 73 | '', 74 | '' 75 | ].join('')); 76 | }); 77 | 78 | app.post('/lambda', function(req, res) { 79 | res.setHeader('Content-Type', 'application/json'); 80 | 81 | debug('Lambda event:'); 82 | debug(req.body.event); 83 | 84 | lambdaLocal.execute({ 85 | event: req.body.event, 86 | lambdaPath: process.argv[2], 87 | lambdaHandler: 'handler', 88 | timeoutMs: 10000, 89 | callback: function(error, data) { 90 | if (error) { 91 | debug('Lambda returned error'); 92 | debug(error); 93 | res.end(JSON.stringify(data)); 94 | } else { 95 | debug('Lambda returned response') 96 | debug(data); 97 | res.end(JSON.stringify(data)); 98 | } 99 | } 100 | }); 101 | }); 102 | 103 | app.listen(port, function() { 104 | console.log('Alexa Skill Test'); 105 | console.log('Skill: ' + skillName); 106 | console.log('Open http://localhost:' + port + ' in your browser'.green) 107 | }); 108 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | import _ from 'lodash' 3 | import $ from 'jquery' 4 | const debug = require('debug')('app') 5 | import shortid from 'shortid' 6 | 7 | export const setRequestType = function(type, firstIntentName) { 8 | debug('Action: setRequestType') 9 | return { 10 | type: types.SET_REQUEST_TYPE, 11 | firstIntentName: firstIntentName, 12 | requestType: type 13 | } 14 | } 15 | 16 | export const setIntentName = function(name) { 17 | debug('Action: setIntentName') 18 | return { 19 | type: types.SET_INTENT_NAME, 20 | intentName: name 21 | } 22 | } 23 | 24 | export const setSlotName = function(name, id, intentName) { 25 | debug('Action: setSlotName') 26 | return { 27 | type: types.SET_SLOT_NAME, 28 | intentName: intentName, 29 | id: id, 30 | name: name 31 | } 32 | } 33 | 34 | export const setSlotValue = function(value, id, intentName) { 35 | debug('Action: setSlotValue') 36 | return { 37 | type: types.SET_SLOT_VALUE, 38 | intentName: intentName, 39 | id: id, 40 | value: value 41 | } 42 | } 43 | 44 | export const setFixedSlot = function(value, name, intentName) { 45 | debug('Action: setFixedSlot') 46 | return { 47 | type: types.SET_FIXED_SLOT, 48 | intentName: intentName, 49 | name: name, 50 | value: value, 51 | id: name 52 | } 53 | } 54 | 55 | export const createSlot = function(intentName) { 56 | debug('Action: createSlot') 57 | return { 58 | type: types.CREATE_SLOT, 59 | intentName: intentName, 60 | name: '', 61 | value: '', 62 | id: shortid.generate() 63 | } 64 | } 65 | 66 | export const deleteSlot = function(id, intentName) { 67 | debug('Action: deleteSlot') 68 | return { 69 | type: types.DELETE_SLOT, 70 | intentName: intentName, 71 | id: id 72 | } 73 | } 74 | 75 | export const doRequest = function(request) { 76 | debug('Action: doRequest') 77 | return function(dispatch) { 78 | $.ajax({ 79 | method: 'post', 80 | url: '/lambda', 81 | data: { 82 | event: request 83 | } 84 | }).done(function(data) { 85 | debug('Action: doRequest done') 86 | dispatch({ 87 | type: types.DO_REQUEST, 88 | response: data 89 | }); 90 | }).fail(function(error, response) { 91 | debug('Action: doRequest error') 92 | dispatch({ 93 | type: types.DO_REQUEST, 94 | response: 'An error occurred' 95 | }); 96 | }); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/components/Skill/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './index.scss' 3 | const debug = require('debug')('app') 4 | import { js_beautify as beautify } from 'js-beautify' 5 | import * as requests from './requests.json' 6 | 7 | class Skill extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.doRequest = this.doRequest.bind(this); 12 | this.createRequest = this.createRequest.bind(this); 13 | this.setRequest = this.setRequest.bind(this); 14 | 15 | this.state = { 16 | request: this.createRequest(props), 17 | validRequest: true 18 | } 19 | } 20 | 21 | createRequest(props) { 22 | debug('Skill Component: createRequest') 23 | 24 | const request = Object.assign({}, requests[props.requestType]) 25 | 26 | if ('intent' === props.requestType) { 27 | request.request.intent.name = '' 28 | request.request.intent.slots = {} 29 | 30 | if (props.intentName) { 31 | request.request.intent.name = props.intentName 32 | 33 | let slotLookUp = 'default' 34 | if (window.INTERACTION_MODEL) { 35 | slotLookUp = props.intentName 36 | } 37 | 38 | if (props.slotsByIntent.get(slotLookUp) && props.slotsByIntent.get(slotLookUp).size >= 1) { 39 | props.slotsByIntent.get(slotLookUp).forEach((slot) => { 40 | request.request.intent.slots[slot.name] = { 41 | name: slot.name, 42 | value: slot.value 43 | } 44 | }) 45 | } 46 | } 47 | } 48 | 49 | return beautify(JSON.stringify(request)) 50 | } 51 | 52 | setRequest(jsonString) { 53 | let validRequest; 54 | try { 55 | let json = JSON.parse(jsonString) 56 | validRequest = true 57 | } catch (error) { 58 | validRequest = false 59 | } 60 | 61 | this.setState({ 62 | request: jsonString, 63 | validRequest: validRequest 64 | }) 65 | } 66 | 67 | componentWillReceiveProps(nextProps) { 68 | this.setState({ 69 | request: this.createRequest(nextProps) 70 | }) 71 | } 72 | 73 | doRequest() { 74 | debug('Skill Component: doRequest'); 75 | if (this.state.validRequest) { 76 | this.props.actions.doRequest(JSON.parse(this.state.request)); 77 | } 78 | } 79 | 80 | render() { 81 | debug('Skill Component: render'); 82 | 83 | const request = this.state.request 84 | const response = beautify(JSON.stringify(this.props.response)) 85 | const requestClass = (this.state.validRequest) ? 'code' : 'invalid code' 86 | 87 | let firstIntentName = null 88 | let slots = [] 89 | 90 | if (window.INTERACTION_MODEL && window.INTERACTION_MODEL.interactionModel && window.INTERACTION_MODEL.interactionModel.languageModel && window.INTERACTION_MODEL.interactionModel.languageModel.intents) { 91 | if (window.INTERACTION_MODEL.interactionModel.languageModel.intents[0]) { 92 | firstIntentName = window.INTERACTION_MODEL.interactionModel.languageModel.intents[0].name 93 | } 94 | 95 | if (this.props.intentName && this.props.slotsByIntent.get(this.props.intentName)) { 96 | slots = this.props.slotsByIntent.get(this.props.intentName) 97 | } else { 98 | let chosenModelIntent = null 99 | 100 | if (this.props.intentName) { 101 | for (let i = 0; i < window.INTERACTION_MODEL.interactionModel.languageModel.intents.length; i++) { 102 | if (this.props.intentName === window.INTERACTION_MODEL.interactionModel.languageModel.intents[i].name) { 103 | chosenModelIntent = window.INTERACTION_MODEL.interactionModel.languageModel.intents[i] 104 | break 105 | } 106 | } 107 | 108 | if (chosenModelIntent && chosenModelIntent.slots) { 109 | chosenModelIntent.slots.forEach((slot) => { 110 | slots.push({ 111 | id: slot.name, 112 | name: slot.name, 113 | value: '' 114 | }) 115 | }) 116 | } 117 | } 118 | } 119 | } else { 120 | if (this.props.slotsByIntent.get('default')) { 121 | slots = this.props.slotsByIntent.get('default') 122 | } 123 | } 124 | 125 | return ( 126 |
127 |
128 |

Alexa Skill Test {window.SKILL_NAME}

129 |
130 | 131 |

Alexa Skill Test lets you easily mock requests to send to your Alexa skill locally. This page will automatically refresh when you update your skill.

132 | 133 |
134 | 135 | 140 |
141 | 142 | {'intent' === this.props.requestType ? 143 |
144 |
145 | 146 | {window.INTERACTION_MODEL && window.INTERACTION_MODEL.interactionModel && window.INTERACTION_MODEL.interactionModel.languageModel && window.INTERACTION_MODEL.interactionModel.languageModel.intents ? 147 | 152 | : 153 | this.props.actions.setIntentName(event.target.value)} /> 154 | } 155 |
156 | 157 |
158 | 159 | 160 | {window.INTERACTION_MODEL && window.INTERACTION_MODEL.interactionModel ? 161 |
162 | {slots.map(function(slot) { 163 | return
164 | 165 | this.props.actions.setFixedSlot(event.target.value, slot.name, this.props.intentName)} value={slot.value} type="text" /> 166 |
167 | }, this)} 168 |
169 | : 170 |
171 | {slots.map(function(slot, index) { 172 | return
173 | this.props.actions.setSlotName(event.target.value, slot.id, 'default')} value={slot.name} type="text" /> 174 | this.props.actions.setSlotValue(event.target.value, slot.id, 'default')} value={slot.value} type="text" /> 175 | 176 | this.props.actions.deleteSlot(slot.id, 'default')}>× 177 |
178 | }, this)} 179 |
180 | this.props.actions.createSlot('default')}>Add a slot 181 |
182 |
183 | } 184 |
185 |
186 | : 187 | '' 188 | } 189 |
190 | 191 |
192 | 193 |
194 |
195 |

Request

196 | 197 |
198 |
199 |

Response

200 |
{response}
201 |
202 |
203 |
204 | ); 205 | } 206 | } 207 | 208 | export default Skill 209 | -------------------------------------------------------------------------------- /src/components/Skill/index.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/variables'; 2 | 3 | .code { 4 | white-space: pre; 5 | font-family: monospace; 6 | border: 1px solid $gray-lighter; 7 | display: inline-block; 8 | width: 100%; 9 | height: 500px; 10 | overflow: scroll; 11 | padding: $padding-base-vertical $padding-base-horizontal; 12 | } 13 | 14 | .invalid { 15 | background-color: rgba(255, 0, 0, 0.13); 16 | } 17 | 18 | .slot input { 19 | display: inline-block; 20 | width: 250px; 21 | margin-right: $padding-base-horizontal * 2; 22 | margin-bottom: $padding-base-vertical * 2; 23 | } 24 | 25 | .delete-slot { 26 | font-size: $font-size-large; 27 | display: inline-block; 28 | } 29 | 30 | .req-resp { 31 | margin-top: $padding-base-vertical * 4; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/Skill/requests.json: -------------------------------------------------------------------------------- 1 | { 2 | "launch": { 3 | "version": "1.0", 4 | "session": { 5 | "new": true, 6 | "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef", 7 | "application": { 8 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 9 | }, 10 | "attributes": {}, 11 | "user": { 12 | "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" 13 | } 14 | }, 15 | "context": { 16 | "System": { 17 | "application": { 18 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 19 | }, 20 | "user": { 21 | "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" 22 | }, 23 | "device": { 24 | "supportedInterfaces": { 25 | "AudioPlayer": {} 26 | } 27 | } 28 | }, 29 | "AudioPlayer": { 30 | "offsetInMilliseconds": 0, 31 | "playerActivity": "IDLE" 32 | } 33 | }, 34 | "request": { 35 | "type": "LaunchRequest", 36 | "requestId": "amzn1.echo-api.request.9cdaa4db-f20e-4c58-8d01-c75322d6c423", 37 | "timestamp": "2015-05-13T12:34:56Z", 38 | "locale": "en-US" 39 | } 40 | }, 41 | "intent": { 42 | "version": "1.0", 43 | "session": { 44 | "new": false, 45 | "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef", 46 | "application": { 47 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 48 | }, 49 | "attributes": {}, 50 | "user": { 51 | "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" 52 | } 53 | }, 54 | "context": { 55 | "System": { 56 | "application": { 57 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 58 | }, 59 | "user": { 60 | "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" 61 | }, 62 | "device": { 63 | "supportedInterfaces": { 64 | "AudioPlayer": {} 65 | } 66 | } 67 | }, 68 | "AudioPlayer": { 69 | "offsetInMilliseconds": 0, 70 | "playerActivity": "IDLE" 71 | } 72 | }, 73 | "request": { 74 | "type": "IntentRequest", 75 | "requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d", 76 | "timestamp": "2015-05-13T12:34:56Z", 77 | "locale": "en-US", 78 | "intent": { 79 | "name": "", 80 | "slots": {} 81 | } 82 | } 83 | }, 84 | "session_end": { 85 | "version": "1.0", 86 | "session": { 87 | "new": false, 88 | "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef", 89 | "application": { 90 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 91 | }, 92 | "attributes": {}, 93 | "user": { 94 | "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" 95 | } 96 | }, 97 | "context": { 98 | "System": { 99 | "application": { 100 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 101 | }, 102 | "user": { 103 | "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" 104 | }, 105 | "device": { 106 | "supportedInterfaces": { 107 | "AudioPlayer": {} 108 | } 109 | } 110 | }, 111 | "AudioPlayer": { 112 | "offsetInMilliseconds": 0, 113 | "playerActivity": "IDLE" 114 | } 115 | }, 116 | "request": { 117 | "type": "SessionEndedRequest", 118 | "requestId": "amzn1.echo-api.request.d8c37cd6-0e1c-458e-8877-5bb4160bf1e1", 119 | "timestamp": "2015-05-13T12:34:56Z", 120 | "locale": "en-US", 121 | "reason": "USER_INITIATED" 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const SET_REQUEST_TYPE = 'SET_REQUEST_TYPE' 2 | export const SET_INTENT_NAME = 'SET_INTENT_NAME' 3 | export const DO_REQUEST = 'DO_REQUEST' 4 | export const SET_SLOT_NAME = 'SET_SLOT_NAME' 5 | export const SET_SLOT_VALUE = 'SET_SLOT_VALUE' 6 | export const SET_FIXED_SLOT = 'SET_FIXED_SLOT' 7 | export const CREATE_SLOT = 'CREATE_SLOT' 8 | export const DELETE_SLOT = 'DELETE_SLOT' 9 | -------------------------------------------------------------------------------- /src/containers/App/index.js: -------------------------------------------------------------------------------- 1 | import Skill from '../../components/Skill'; 2 | import React from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import * as Actions from '../../actions' 6 | import PropTypes from 'prop-types' 7 | import './index.scss' 8 | 9 | class App extends React.Component { 10 | render() { 11 | return ( 12 |
13 | 14 |
15 | ) 16 | } 17 | } 18 | 19 | App.propTypes = { 20 | slotsByIntent: PropTypes.object.isRequired, 21 | intentName: PropTypes.string.isRequired, 22 | requestType: PropTypes.string.isRequired, 23 | response: PropTypes.object.isRequired, 24 | actions: PropTypes.object.isRequired 25 | } 26 | 27 | const mapStateToProps = state => ({ 28 | requestType: state.get('requestType'), 29 | intentName: state.get('intentName'), 30 | response: state.get('response'), 31 | slotsByIntent: state.get('slotsByIntent') 32 | }) 33 | 34 | const mapDispatchToProps = dispatch => ({ 35 | actions: bindActionCreators(Actions, dispatch) 36 | }) 37 | 38 | export default connect( 39 | mapStateToProps, 40 | mapDispatchToProps 41 | )(App) 42 | -------------------------------------------------------------------------------- /src/containers/App/index.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap-sprockets'; 2 | @import 'bootstrap'; 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore, applyMiddleware } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import App from './containers/App' 6 | import reducer from './reducers' 7 | import thunk from 'redux-thunk' 8 | const debug = require('debug')('app') 9 | 10 | const store = createStore(reducer, applyMiddleware(thunk)); 11 | 12 | if (DEBUG) { 13 | localStorage.debug = 'app' 14 | } 15 | 16 | debug('Running Alexa Skill Test in debug mode.') 17 | 18 | render( 19 | 20 | 21 | , 22 | document.getElementById('root') 23 | ); 24 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { SET_REQUEST_TYPE, SET_INTENT_NAME, SET_SLOT_NAME, DELETE_SLOT, SET_FIXED_SLOT, SET_SLOT_VALUE, SET_REQUEST, DO_REQUEST, CREATE_SLOT } from '../constants/ActionTypes' 2 | import immutable from 'immutable' 3 | const debug = require('debug')('app') 4 | 5 | const initialState = immutable.Map({ 6 | slotsByIntent: immutable.Map(), 7 | requestType: 'launch', 8 | intentName: '', 9 | response: {} 10 | }); 11 | 12 | export default function(state = initialState, action) { 13 | debug('Reduce: ' + action.type) 14 | 15 | switch (action.type) { 16 | case SET_REQUEST_TYPE: 17 | if (action.firstIntentName) { 18 | state = state.set('intentName', action.firstIntentName) 19 | } 20 | 21 | return state.set('requestType', action.requestType) 22 | case SET_INTENT_NAME: 23 | return state.set('intentName', action.intentName) 24 | case SET_FIXED_SLOT: 25 | var slotsByIntent = state.get('slotsByIntent') 26 | var intentSlots = slotsByIntent.get(action.intentName) 27 | var updated = false 28 | 29 | if (!intentSlots) { 30 | intentSlots = immutable.List() 31 | 32 | intentSlots = intentSlots.push({ 33 | name: action.name, 34 | id: action.name, 35 | value: action.value 36 | }) 37 | 38 | return state.set('slotsByIntent', slotsByIntent.set(action.intentName, intentSlots)) 39 | } 40 | 41 | intentSlots = intentSlots.map((slot) => { 42 | if (slot.name === action.name) { 43 | slot.value = action.value 44 | updated = true 45 | } 46 | 47 | return slot 48 | }) 49 | 50 | if (!updated) { 51 | intentSlots = intentSlots.push({ 52 | name: action.name, 53 | id: action.name, 54 | value: action.value 55 | }) 56 | } 57 | 58 | return state.set('slotsByIntent', slotsByIntent.set(action.intentName, intentSlots)) 59 | case CREATE_SLOT: 60 | var slotsByIntent = state.get('slotsByIntent') 61 | var intentSlots = slotsByIntent.get(action.intentName) 62 | 63 | if (!intentSlots) { 64 | intentSlots = immutable.List() 65 | } 66 | 67 | intentSlots = intentSlots.push({ 68 | id: action.id, 69 | name: action.name, 70 | value: action.value 71 | }) 72 | 73 | return state.set('slotsByIntent', slotsByIntent.set(action.intentName, intentSlots)) 74 | case SET_SLOT_NAME: 75 | var slotsByIntent = state.get('slotsByIntent') 76 | var intentSlots = slotsByIntent.get(action.intentName) 77 | 78 | if (!intentSlots) { 79 | return state 80 | } 81 | 82 | intentSlots = intentSlots.map((slot) => { 83 | if (slot.id === action.id) { 84 | slot.name = action.name 85 | } 86 | 87 | return slot 88 | }) 89 | 90 | return state.set('slotsByIntent', slotsByIntent.set(action.intentName, intentSlots)) 91 | case DELETE_SLOT: 92 | var slotsByIntent = state.get('slotsByIntent') 93 | var intentSlots = slotsByIntent.get(action.intentName) 94 | 95 | if (!intentSlots) { 96 | return state 97 | } 98 | 99 | intentSlots = intentSlots.filter((slot) => { 100 | if (slot.id === action.id) { 101 | return false 102 | } 103 | 104 | return true 105 | }) 106 | 107 | return state.set('slotsByIntent', slotsByIntent.set(action.intentName, intentSlots)) 108 | case SET_SLOT_VALUE: 109 | var slotsByIntent = state.get('slotsByIntent') 110 | var intentSlots = slotsByIntent.get(action.intentName) 111 | 112 | if (!intentSlots) { 113 | return state 114 | } 115 | 116 | intentSlots = intentSlots.map((slot) => { 117 | if (slot.id === action.id) { 118 | slot.value = action.value 119 | } 120 | 121 | return slot 122 | }) 123 | 124 | return state.set('slotsByIntent', slotsByIntent.set(action.intentName, intentSlots)) 125 | case DO_REQUEST: 126 | return state.set('response', action.response) 127 | default: 128 | return state 129 | } 130 | }; 131 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: __dirname + '/src/index.js', 6 | devtool: 'inline-source-map', 7 | watch: true, 8 | output: { 9 | filename: 'dist/client.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.scss$/, 15 | use: [ 16 | { 17 | loader: "style-loader" 18 | }, 19 | { 20 | loader: "css-loader" 21 | }, 22 | { 23 | loader: "sass-loader", 24 | options: { 25 | includePaths: [path.join(__dirname, 'node_modules/bootstrap-sass/assets/stylesheets'), 'node_modules'] 26 | } 27 | } 28 | ] 29 | }, 30 | { 31 | test: /\.jsx?$/, 32 | include: [ 33 | path.resolve(__dirname, 'src') 34 | ], 35 | exclude: [ 36 | path.resolve(__dirname, 'node_modules') 37 | ], 38 | loader: 'babel-loader' 39 | } 40 | ] 41 | }, 42 | stats: { 43 | colors: true 44 | }, 45 | resolveLoader: { 46 | modules: [path.join(__dirname, 'node_modules'), 'node_modules'] 47 | }, 48 | resolve: { 49 | modules: [path.join(__dirname, 'node_modules'), 'node_modules'] 50 | }, 51 | plugins: [ 52 | new webpack.DefinePlugin({ 53 | DEBUG: (process.env.DEBUG && process.env.DEBUG.match(/app/i)) ? true : false 54 | }) 55 | ] 56 | }; 57 | --------------------------------------------------------------------------------