├── .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 |
169 | :
170 |
171 | {slots.map(function(slot, index) {
172 | return
178 | }, this)}
179 |
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 |
--------------------------------------------------------------------------------