160 Commits

Author SHA1 Message Date
Bilal
a5c09206c9 delete obsolete file 2018-04-18 00:12:17 +00:00
GotPPay
95b8fea01a close session after finishing intent 2018-04-18 00:26:41 +02:00
Bilal
26e186bf3a verify request from amazon 2018-04-17 16:50:28 +00:00
Bilal
4d2c4891cc Merge branch 'master' of https://github.com/GotPPay/tellall 2018-04-16 10:43:15 +00:00
Senad Uka
d281dda327 Merge pull request #17 from GotPPay/summarizer-feature
4b. Summarizer feature
2018-04-16 12:12:50 +02:00
Senad Uka
9529104632 Merge pull request #18 from GotPPay/tests-for-import-from-source
Tests for components related to 'import from source' part
2018-04-16 12:12:42 +02:00
GotPPay
70845d5594 Merge branch 'tests-for-import-from-source' into summarizer-feature 2018-04-15 16:33:03 +02:00
GotPPay
5a1d878150 flip logic (comment from PR #18) 2018-04-15 16:28:19 +02:00
GotPPay
b5d66c950a improve code (comment from PR #17) 2018-04-11 11:00:33 +02:00
GotPPay
d69972da07 Merge branch 'tests-for-import-from-source' into summarizer-feature 2018-04-11 10:54:34 +02:00
GotPPay
4ac7284724 improved code readability (comment from PR #18) 2018-04-11 10:49:12 +02:00
GotPPay
e138c6e09e improved code readability (comment from PR #18 2018-04-11 10:42:06 +02:00
GotPPay
0634c3898a fix typo 2018-04-06 22:33:53 +02:00
GotPPay
9dce82b28c Merge branch 'tests-for-import-from-source' into summarizer-feature 2018-04-06 14:53:41 +02:00
GotPPay
83134841a2 add test for question delete; fix bug with delete question 2018-04-05 15:40:10 +02:00
GotPPay
275ab2e9b1 created tests for IntentDetails component ; improved input validators 2018-04-04 21:59:13 +02:00
GotPPay
4d2cf52e4c add snapshot tests for 'import-from-source' part 2018-04-04 01:09:29 +02:00
GotPPay
ecb06fd4e2 created tests for components related to importing from source 2018-04-03 23:45:53 +02:00
GotPPay
c838c6074d apply changes from PR #15 2018-04-03 15:56:13 +02:00
Bilal
f64155e061 Merge pull request #15 from GotPPay/import-from-source
4a. Import from source
2018-04-03 15:01:39 +02:00
GotPPay
ce79c0e1e7 improved search for same questions and question variants 2018-04-03 14:49:36 +02:00
GotPPay
4a43dda852 fixed PR #15 comments 2018-04-03 14:19:07 +02:00
GotPPay
ec3d4dcd6c fix bug with undefined variable ; improve abstraction 2018-04-02 12:55:31 +02:00
GotPPay
38c63e619b use length control for summary 2018-04-02 09:44:56 +02:00
GotPPay
6643aafb54 clean text before summarization 2018-04-02 04:21:27 +02:00
GotPPay
e04dbe3ec9 clean answer from html tags so it can be recited by alexa 2018-04-02 04:15:36 +02:00
GotPPay
e4b1617dc1 change summarizing lib 2018-04-02 03:41:15 +02:00
GotPPay
cd74990165 4b. introduce summarizer ; use summarizer for top news reading 2018-04-01 22:32:20 +02:00
GotPPay
284cdcd7ba fix promise answer 2018-03-30 14:30:30 +02:00
GotPPay
0ebadcd3f7 promisify answer on user defined question 2018-03-30 12:08:38 +02:00
GotPPay
f163dde5b2 wait until unswer is ready 2018-03-30 11:52:04 +02:00
GotPPay
431daa3182 return answer instead of promise 2018-03-30 11:43:44 +02:00
GotPPay
fae0e0db23 return result instead of promise 2018-03-30 11:26:42 +02:00
GotPPay
443dc53dbd 4a. import content from WP ; change design to reflect 4a 2018-03-30 10:54:15 +02:00
MirnaM
5484a9a461 Add ui for answers from source 2018-03-28 15:51:10 +02:00
MirnaM
85d1b01dab Merge pull request #14 from GotPPay/handle-user-input
Handle user input
2018-02-07 11:06:37 +01:00
GotPPay
779e2d61f4 backend user input handling 2018-01-29 23:23:36 +01:00
GotPPay
5e92314938 frontend user input handling 2018-01-29 21:32:24 +01:00
GotPPay
3b5c287ef9 . 2018-01-27 10:53:06 +01:00
GotPPay
7ffbf42da9 update tokens 2018-01-27 03:15:47 +01:00
GotPPay
6b4256b7ab switch to saburly amazon account 2018-01-26 23:21:59 +01:00
GotPPay
e712693dd0 switch to saburly amazon account 2018-01-26 23:21:18 +01:00
GotPPay
220d76c2be test saburly account 2018-01-26 17:46:16 +01:00
GotPPay
2870695b25 Fix regex 2018-01-26 15:35:38 +01:00
GotPPay
bca71604fb Allow only letters for question name 2018-01-25 17:42:17 +01:00
GotPPay
84edb5563f prevent space in question name 2018-01-25 16:09:12 +01:00
MirnaM
bad8ccdce3 Merge pull request #13 from GotPPay/implement-built-in-intents
Implement built in intents
2018-01-25 15:59:16 +01:00
GotPPay
9b58f7745b improve code 2018-01-25 15:56:45 +01:00
GotPPay
b0737efb4e add missing spread operator 2018-01-25 15:39:48 +01:00
GotPPay
5d2a15bc93 apply comment from PR 2018-01-25 15:19:30 +01:00
GotPPay
145fff3b51 fix alexa bug with no speak action 2018-01-24 13:14:47 +01:00
GotPPay
4f6c714fa3 Rephrase continuation question 2018-01-24 13:11:35 +01:00
GotPPay
f714fdf70a improve No intent 2018-01-24 13:07:08 +01:00
GotPPay
adc1f1c099 improve default built-in intents 2018-01-24 13:02:46 +01:00
GotPPay
c0a177d39b experiment with yes no help cancel 2018-01-23 14:40:01 +01:00
GotPPay
3700d9bb58 Simulate built-in intents 2018-01-23 13:46:02 +01:00
GotPPay
7a5ddc6b52 Experiment with user-defined help intent 2018-01-23 13:33:53 +01:00
GotPPay
39b8e7608e Test built-in intents 2018-01-23 13:24:56 +01:00
Bilal
47df972033 . 2018-01-23 11:24:32 +00:00
MirnaM
a8ffe0a5c0 Merge pull request #11 from GotPPay/send-message-feature
Send message feature
2018-01-23 12:19:57 +01:00
MirnaM
def591224e Merge pull request #12 from GotPPay/question-explanation
Question explanation
2018-01-23 12:19:14 +01:00
GotPPay
d8b2f5f0b4 fix typo 2018-01-23 12:18:20 +01:00
GotPPay
dc2c8f384e use foreach instead of map 2018-01-23 12:15:04 +01:00
GotPPay
cc579133c0 Amazon default intents not working with dialog 2018-01-23 02:23:59 +01:00
GotPPay
0a8eb2e280 fix code 2018-01-23 02:18:08 +01:00
GotPPay
6b26db3d18 fix code 2018-01-23 02:15:44 +01:00
GotPPay
3202bf5f0b Implement help intent 2018-01-23 01:56:07 +01:00
GotPPay
370edd6ef0 fix inherited explanation bug 2018-01-23 01:51:36 +01:00
GotPPay
0d858ad1c7 List only questions with explanation 2018-01-23 01:46:02 +01:00
GotPPay
c6cd49a66f fix typo 2018-01-23 01:41:34 +01:00
GotPPay
97b6755f2f No magic numbers 2018-01-23 01:40:19 +01:00
GotPPay
b2386ea0d6 Yes and No intent dont work with dialog 2018-01-23 01:03:59 +01:00
GotPPay
2ae983d211 Experiment with Yes and No intent 2018-01-23 00:40:19 +01:00
GotPPay
b1a853c363 experiment with Yes and No intents 2018-01-23 00:20:50 +01:00
GotPPay
d58d4b89e3 experiment with yes intent 2018-01-23 00:08:39 +01:00
GotPPay
2c6953fe97 list all questions on Launch 2018-01-22 23:35:25 +01:00
GotPPay
af19108e9c fix InteractionModel generator 2018-01-22 22:34:58 +01:00
GotPPay
a00859c594 fix InteractionModel generator 2018-01-22 22:34:13 +01:00
GotPPay
085a0324b3 . 2018-01-22 20:12:16 +01:00
GotPPay
d8799fa40d fix JSON model 2018-01-19 23:26:59 +01:00
GotPPay
de0a4f3176 fix reverting skill on failed update 2018-01-19 22:54:50 +01:00
GotPPay
48578d3ffe Change UI 2018-01-19 22:42:37 +01:00
GotPPay
e2d648980b fix code 2018-01-19 20:28:26 +01:00
GotPPay
b73086291c code fix and improvements 2018-01-19 20:23:50 +01:00
GotPPay
434d45248c fix error in validation and undefined email value 2018-01-19 20:15:27 +01:00
GotPPay
00272ec67d validate email using regex 2018-01-19 20:05:49 +01:00
GotPPay
0727328e58 improve email preview 2018-01-19 19:53:49 +01:00
GotPPay
e2d76ff03e improve email preview 2018-01-19 19:50:21 +01:00
GotPPay
9cff3cd9ae change email service 2018-01-19 19:28:46 +01:00
GotPPay
2f82709f11 improve dialog ; send email 2018-01-19 16:29:47 +01:00
MirnaM
f6bf3a05b2 Merge pull request #10 from GotPPay/switch-to-alexa-sdk
Switch to alexa sdk
2018-01-19 11:24:23 +01:00
GotPPay
4b594898b1 Complete dialogtest 2018-01-19 08:00:51 +01:00
GotPPay
b07a9e21b3 Testing dialog handler 2018-01-19 07:43:10 +01:00
GotPPay
a570640fe1 hande unknown questions 2018-01-18 22:07:23 +01:00
GotPPay
9e4b06bd4c keep session open - fix 2018-01-18 21:56:56 +01:00
GotPPay
6bfd4adcaf keep session open 2018-01-18 21:50:26 +01:00
GotPPay
48badce0f0 fix 'res not defined' 2018-01-18 21:43:25 +01:00
GotPPay
68287d49ff complete switch to new alexa package 2018-01-18 21:33:44 +01:00
GotPPay
2f59e12aa7 test new library 2018-01-18 19:50:13 +01:00
MirnaM
3e37c93395 Merge pull request #7 from GotPPay/inconsistent-state-fix
Inconsistent state fix
2018-01-16 17:05:21 +01:00
GotPPay
cd041c0131 code improvements 2018-01-16 16:56:56 +01:00
MirnaM
0607f1e98d Merge pull request #9 from GotPPay/UI-text-fix
Ui text fix
2018-01-16 14:48:51 +01:00
GotPPay
5db270d957 fix missing css on build 2018-01-16 13:48:39 +01:00
GotPPay
389a71b05a UI fix 2018-01-16 13:38:05 +01:00
GotPPay
e5783740cf fix token expired state 2018-01-16 01:44:33 +01:00
GotPPay
1a5f4586a8 inconsistent state fix 2018-01-16 00:40:03 +01:00
GotPPay
a78189f1a1 removed doubled function ; return object instead of array with one element 2018-01-16 00:08:38 +01:00
GotPPay
cd938c3981 additional edit for api refactoring 2018-01-15 23:44:50 +01:00
MirnaM
2bce02ab4e Merge pull request #6 from GotPPay/api-refactoring
Api refactoring
2018-01-15 16:34:33 +01:00
GotPPay
6859f567a9 frontend api refactoring 2018-01-15 16:27:12 +01:00
GotPPay
6e7bccea86 frontend api refactoring 2018-01-15 16:09:41 +01:00
GotPPay
7e4b959ffa backend api refactoring 2018-01-15 16:09:09 +01:00
GotPPay
e1f315cb81 backend api refactoring 2018-01-15 16:06:37 +01:00
MirnaM
49d30f00f8 Merge pull request #5 from GotPPay/step-4
Step 4
2018-01-15 14:56:03 +01:00
GotPPay
1b04e72b98 Revert changes 2018-01-15 14:52:34 +01:00
GotPPay
07857fd2f4 frontend code refactoring 2018-01-14 01:00:35 +01:00
GotPPay
7f56a28509 backend code refactoring 2018-01-13 14:57:41 +01:00
GotPPay
ff7e564d2e conflict fix 2018-01-12 08:23:34 +01:00
GotPPay
bbec4e9940 . 2018-01-12 02:12:05 +01:00
GotPPay
b80843cb97 ... 2018-01-12 01:56:17 +01:00
Bilal
4c8c1c5e0e change utterances to correct form;change intents to mimic dialog; 2018-01-11 14:05:44 +00:00
GotPPay
7d79c03d15 initial step 4 for online test 2018-01-11 04:24:16 +01:00
GotPPay
3214a2bea4 fixed delete bug, code refactoring, improved UI 2018-01-10 13:27:09 +01:00
Bilal
84c11f1b2f Keep session open 2018-01-08 21:13:23 +00:00
GotPPay
1a9227e76d refactoring 2018-01-08 21:50:24 +01:00
GotPPay
cd48599f39 Update readme 2018-01-08 13:53:50 +01:00
GotPPay
61bd6862aa removed unnecessary function 2018-01-08 13:49:24 +01:00
Bilal
994670ce54 from server ; working with amazon test service, not working with real device 2018-01-08 12:44:38 +00:00
Bilal
51ff7f255c from server ; intents working - launch request not working 2018-01-08 12:06:25 +00:00
Bilal
8a59d452f5 from server 2018-01-08 10:33:15 +00:00
Bilal
7fbc105433 .. 2018-01-07 17:51:07 +00:00
GotPPay
44ef80b7dd . 2018-01-07 17:53:46 +01:00
Bilal
675861d2cd almost there 2018-01-07 16:39:21 +00:00
Bilal
9ba9209aa3 working frontend ; backend not working 2018-01-07 13:54:15 +00:00
Bilal
64ff4357d1 Merge branch 'bilal-step-3-token-refresh' of https://github.com/GotPPay/tellall into bilal-step-3-token-refresh 2018-01-05 23:00:00 +00:00
GotPPay
3fe9ebde9d new scss file 2018-01-05 23:59:26 +01:00
Bilal
f1e4010950 from server 2018-01-05 22:28:55 +00: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
Bilal
db5bd107a0 add view engine 2018-01-05 19:40:05 +00:00
Bilal
214e6dff71 Merge branch 'bilal-step-3-token-refresh' of https://github.com/GotPPay/tellall into bilal-step-3-token-refresh 2018-01-05 19:33:48 +00:00
GotPPay
c3bba7ffa7 express routes reorder 2018-01-05 20:32:55 +01:00
Bilal
1f05d227b2 Changes from server 2018-01-05 19:20:30 +00: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
GotPPay
124885b41d clean backend 2017-12-03 18:47:24 +01:00
GotPPay
e8ee77a40d remove unused items 2017-12-03 18:25:10 +01:00
GotPPay
94d2883c01 send update to amazon 2017-12-03 17:38:58 +01:00
GotPPay
74ee9de93f generate interaction model 2017-12-03 00:15:11 +01:00
GotPPay
d5120a1ba2 functional application without amazon update 2017-12-02 22:48:45 +01:00
MirnaM
bb9d270238 Add skill interaction model 2017-12-02 20:20:59 +01:00
MirnaM
81c9dde0ef Merge branch 'master' into bilal-step3 2017-12-01 15:07:39 +01:00
MirnaM
d375d79e67 Merge branch 'bilal-step3' of github.com:GotPPay/tellall into bilal-step3 2017-12-01 15:05:34 +01:00
MirnaM
819dbefde7 Find intent object by name and send answer as response 2017-12-01 14:37:51 +01:00
GotPPay
805deab2d9 clean 2017-12-01 14:35:05 +01:00
GotPPay
0e193fa5a8 new design 2017-12-01 11:03:48 +01:00
GotPPay
4f36fc7738 stage 3 2017-11-30 17:43:24 +01:00
69 changed files with 22806 additions and 713 deletions

104
README.md Normal file
View File

@@ -0,0 +1,104 @@
To obtain client ID and secret :
https://developer.amazon.com/lwa/sp/overview.html
Click create new security profile, and add whitelist redirect uri : https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO
To obtain new Auth Code :
https://www.amazon.com/ap/oa?client_id=amzn1.application-oa2-client.8c183daec15c488c9126b62bda9f7832&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
}
=======================
=======================
Prerequests for step 3 (run on server):
requires running mongodb service
Database (tellall) with collection (skill_list)
* Insert dummy skill with : db.skill_list.insert({"skillID" : "amzn1.ask.skill.2445552d-954d-4cd6-b77f-295368e02842", "intents" : [ { "intentName" : "GetFirstQuestion", "questionExplanation" : "","questions" : [ "tell me something about projects", "tell me all about projects" ], "answer" : "blablabla bla bla", "answerType":0, "externalAnswerSource":"" }, { "intentName" : "GetThirdQuestion", "questionExplanation" : "","questions" : [ "Give me third question" ], "answer" : "This is answer to the third question", "answerType":1, "externalAnswerSource":"http://sarajevotimes.com" } ], "invocationName" : "saburly", "invocationAnswer" : "We are Saburly team one", "contactEmail":"bilal@saburly.com" })
*obtain _id and change in web/src/App.js, and also skill_db_id in backend/config.js
*enter web/ dir and run "npm run build"
Database (tellall) with collection (token_list)
* Insert tokens with : db.token_list.insert({"id" : 1, "refresh_token" : "...", "access_token" : "...", "expires_in" : 1515173601.754 })
(Change refresh_token and access_token dots with real ones)
Set skill_id, client_id and client_secret to appropriate values in backend/config.js
Set base_url to "tellall.saburly.com" in web/src/config.
Start backend service from backend/ running "node express.js"
======
for local testing :
first terminal :
*cd web
*npm start
second terminal :
* cd backend
*npm install
*node express.js

33
backend/config/config.js Normal file
View File

@@ -0,0 +1,33 @@
constants = require('./constants')
var config = {};
config.SKILL_STAGE = constants.skillStage.IN_DEVELOPMENT;
config.DB_URL = 'mongodb://localhost:27017/tellall';
config.PORT = 5000;
//Bilal TOKENS
//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';
//Saburly TOKENS
config.TOKEN = 'Atza|IwEBIABS0RvlVshGGO64X0tabhUuzpJKjbWNgxpRiy7YTftFD_lWlp-cbeXuVjRAu8kior2W2C5swf_rEHYvELQXdw78gB9WJQh4ITncPgqNCxvEnwVpIXiyeC_O287DRErnTYbI3s34i4NcxrXzobB8fIpTZxOkg6BI6vQGvvaiNLsTim2ElDYgAEmdgTN77llcMai521aovDqEw_XFc6GspeXhiGKxRomCMIL2UaT649owapDQ3y3Ug9eHvEaBzqjYdOUAtRtv19BGkG8YPs3npHmP5AD0Oc3ByCfrofcGk7fdd_nq28pRX6h4LXk4ylM279qlneWh9EErsWh8vtWuGEGusnDxW17OzEzf7HuwNDqdCJ6gCrIEkZaHISrSQ-vTsYGhKbv0z4nNjf_W_aoc9UJr9LnISCXx424R--iGDKZXhYWlZRjaiHsXE33MpS_M-sdN6GXYQwIjanTUahVXh5h-IBam5uJzTejE6CkIh5iUJ6um2IlDelJRMGS-T_aaG3zUvEagvEd9V9Z5mVN_kmO8bH4H2VefZuFGHRsCPa6SoLrlN0rkXK5fMw-zXfV2MHvQFdkgqYqGKxiEwWJ-g4n1ZrcPtWQowHT2z2yWrfnM2A6g8GIdPT23znmRcrdz4EU';
config.REFRESH_TOKEN = 'Atzr|IwEBIENdBZntrzvJYesv8SGhnty4Nyk2ZySL09elw5N0wH8S1Brz1UgIYLqenw3sKKxnc-VrIUbNtl1Ka4GDKwcTr2fDU_AbKQ6YXzeRBrfRQVvNOeCtjZE8P6Kg1PxAeQoCsqo7WPxK8ZdUaLwPjt_xiZ1FXtr01g-211PJs4KEg5jyF5nY2S14jA_TbwDW6ihpNqWd6ZklTZSRaOeSGa1mXZCSZ5yTsZIQV1Pn0fKhCXtcVg2L833YqRmextmHij4-2NtBQdW3gif5MPdhYjTqDNwgxO3OOagK1uSFqXOnMcmEDnxZuQQApugfDzClfN6DiDALCKN4dVAX8-OU_L2xsUkKiFP9rQjvHWJoRFBT1FpXjBfoVyzM1AaJ6C83WX6SjOBE0hhikQKIaPSe1ikK8_MzOIs2wqLLPnLGnKj3kcKMkDmY6DMgxfWj2H0hwLy0oZ7_qykS7wUHMGjRO5yScuBWFIr0RFFu00GS7zrKjkhFc_E4ZBBKskn0gywS-5pogo0U1rQtLg8lQJsVbxXQwtq2TxkGFMBiVxtQcXtHM1qbCVXpZQbk7ezxvasj4yAIsF4H7KBiZSGmWHkk4yADykWJSntTjFcM2wG0wSiEYoYzJQ';
config.TOKEN_EXPIRES_IN = 1515100500;
//config.SKILL_ID = 'amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae'; //bilal
config.SKILL_ID = 'amzn1.ask.skill.2445552d-954d-4cd6-b77f-295368e02842'; //saburly
//config.SKILL_DB_ID = '5a5016e775becaef2015da10'; //for server
config.SKILL_DB_ID = '5abd461329f85e4ec728d945'; //for local
//Bilal
//config.CLIENT_ID = 'amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7';
//config.CLIENT_SECRET = '6dea8125cecd049d3c4cff7bb5bdfd3ff17bc6fed246c4c8f6b519d9ed08d0b3';
//Saburly
config.CLIENT_ID = 'amzn1.application-oa2-client.8c183daec15c488c9126b62bda9f7832';
config.CLIENT_SECRET = '3acaa0755291132ee11e1cceaa100feef96a0244662df712a52189199cc655de';
module.exports = config;

View File

@@ -0,0 +1,84 @@
const constants = {};
constants.skillStage = {
IN_DEVELOPMENT: 'development',
LIVE: 'live',
};
constants.amazonResultCodes = {
OK: 200,
ACCEPTED: 202,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
NOT_FOUND: 404,
CONFLICT: 409,
PAYLOAD_TOO_LARGE: 413,
};
constants.apiResultCodes = {
GENERIC_ERROR: -1,
OK: 0,
AMAZON_ERROR: 1, //amazon api works, but error is some of the amazonResultCodes
AMAZON_FAIL: 2, //amazon api doesn't work
DATABASE_ERROR: 3,
NO_SKILL: 4,
INCONSISTENT_STATE: 5,
INVALID_SKILL: 6,
};
constants.HTTPResultCodes = {
INTERNAL_SERVER_ERROR: 500,
};
constants.SKILL_ID_LENGTH = 24;
constants.voiceResponseStrings = {
QUESTION_NOT_FOUND: 'Sorry, I didnt understand',
GENERIC_CONTINUE: 'Say something to continue',
DIDNT_ASK_ANYTHING: 'There was no question to answer to',
ERROR_SUMMARIZING_CONTENT: 'Sorry, there was problem with summarizing news',
ERROR_FETCHING_CONTENT: 'Failed to get content',
};
//Timing is given in [ms]
constants.voiceResponseTimings = {
PAUSE_BETWEEN_QUESTIONS: 650,
PAUSE_AFTER_WELCOME_MESSAGE: 650,
PAUSE_BETWEEN_TITLES: 500,
PAUSE_BETWEEN_TITLE_AND_CONTENT: 500,
PAUSE_BETWEEN_NEWS: 800,
};
constants.stringConstraints = {
INTENT_EXPLANATION_MAX_LENGTH: 70,
INTENT_NAME_MAX_LENGTH: 30,
INTENT_NAME_MIN_LENGTH: 2,
QUESTION_MAX_LENGTH: 150,
QUESTION_MIN_LENGTH: 2,
ANSWER_MAX_LENGTH: 150,
ANSWER_MIN_LENGTH: 2,
INVOCATION_NAME_MAX_LENGTH: 50,
INVOCATION_NAME_MIN_LENGTH: 2,
INVOCATION_ANSWER_MAX_LENGTH: 100,
EMAIL_MAX_LENGTH: 100,
};
constants.answerType = {
PREDEFINED: 0,
EXTERNAL_SOURCE_WP_TITLES: 1,
EXTERNAL_SOURCE_WP_NEWS: 2,
};
constants.contentType = {
TITLES: 0,
NEWS: 1,
};
constants.FIXED_SUMMARY_LENGTH = 3;
module.exports = constants;

14
backend/config/email.js Normal file
View File

@@ -0,0 +1,14 @@
var config = {};
config.PORT = 587;
config.SMTP_HOST = 'smtp.mail.com';
config.SECURE = false;
config.AUTH = {
user: 'saburly@mail.com',
pass: 'KeepSaburly',
};
config.FROM_EMAIL = 'saburly@mail.com';
config.SUBJECT = 'Message from Saburly service';
module.exports = config;

View File

@@ -0,0 +1,6 @@
var express = require ('express'), router = express.Router ();
router.use ('/skill', require ('./skill'));
router.use ('/saburly', require('./saburlyEntryPoint'));
module.exports = router;

View File

@@ -0,0 +1,12 @@
var express = require ('express'), router = express.Router ();
var bodyParser = require ('body-parser');
var alexa = require ('../models/alexa');
var verifier = require('alexa-verifier-middleware')
router.use(verifier);
router.post ('/', bodyParser.json (), async (req, res) => {
alexa.run (req, res);
});
module.exports = router;

View File

@@ -0,0 +1,145 @@
let express = require ('express'), router = express.Router ();
const constants = require ('../config/constants');
let databaseHelper = require ('../helpers/database');
let amazonHelper = require ('../helpers/amazon');
let skillValidator = require('../helpers/skillValidator');
let bodyParser = require ('body-parser');
let alexa = require ('../models/alexa');
router.get ('/:id', async (req, res, next) => {
const id = req.params.id;
if (id.length !== constants.SKILL_ID_LENGTH) {
res.json ([]);
} else {
databaseHelper
.getSkill (id)
.then (result => {
res.json (result);
})
.catch (err => {
res.json ([]);
});
}
});
router.put ('/:id', bodyParser.json (), async (req, res, next) => {
let id = req.params.id;
let dataFromWeb = JSON.stringify (req.body);
let skill = JSON.parse (dataFromWeb);
let updateOnAmazon = skill.updateOnAmazon;
delete skill.updateOnAmazon;
delete skill._id;
//Validate skill
if (!skillValidator.validateSkill(skill)){
//skill not valid
res
.status (
constants.HTTPResultCodes.INTERNAL_SERVER_ERROR
)
.json ({
result: constants.apiResultCodes.INVALID_SKILL,
message: '',
});
return;
}
//First get current skill from DB
databaseHelper
.getSkill (id)
.then (currentSkillState => {
//Now let's update skill in DB
databaseHelper
.updateSkill (id, skill)
.then (() => {
//Ok, done, now update skill on Amazon (if needed)
if (updateOnAmazon) {
//We need to update skill on Amazon
amazonHelper
.updateSkill (skill)
.then (amazonResult => {
if (
amazonResult === constants.amazonResultCodes.OK ||
amazonResult === constants.amazonResultCodes.ACCEPTED
) {
res.json ({result: constants.apiResultCodes.OK, message: ''});
alexa.updateModel ();
} else {
//Update on amazon failed, revert changes in database and send error to user
databaseHelper
.updateSkill (id, currentSkillState)
.then (() => {
res
.status (
constants.HTTPResultCodes.INTERNAL_SERVER_ERROR
)
.json ({
result: constants.apiResultCodes.AMAZON_ERROR,
message: amazonResult,
});
})
.catch (() => {
//This should never happen, something is seriously wrong, like no database connection
res
.status (
constants.HTTPResultCodes.INTERNAL_SERVER_ERROR
)
.json ({
result: constants.apiResultCodes.INCONSISTENT_STATE,
message: '',
});
});
}
})
.catch (e => {
//Update on amazon failed, revert changes in database and send error to user
databaseHelper
.updateSkill (id, currentSkillState)
.then (() => {
res
.status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR)
.json ({
result: constants.apiResultCodes.AMAZON_FAIL,
message: e,
});
})
.catch (() => {
//This should never happen, something is seriously wrong, like no database connection
res
.status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR)
.json ({
result: constants.apiResultCodes.INCONSISTENT_STATE,
message: '',
});
});
});
} else {
//No need to update on Amazon, tell to user it's ok
res.json ({result: constants.apiResultCodes.OK, message: ''});
alexa.updateModel ();
}
})
.catch (() => {
//Update in database didn't go well, no need to revert since it failed to write in the first place
//just send error to user
res
.status (
constants.HTTPResultCodes.INTERNAL_SERVER_ERROR
)
.json ({
result: constants.apiResultCodes.DATABASE_ERROR,
message: '',
});
});
})
.catch (e => {
//I don't know why, but something went wrong, possibly ID of skill is wrong, doesn't exist in DB
res
.status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR)
.json ({result: constants.apiResultCodes.NO_SKILL, message: ''});
});
});
module.exports = router;

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

@@ -0,0 +1,311 @@
require ('isomorphic-fetch');
const config = require ('../config/config');
var request = require ('request');
var databaseHelper = require ('./database');
var getBuildStatus = function (skillID) {
fetch (
`https://api.amazonalexa.com/v0/skills/${skillID}/interactionModel/locales/en-US/status`,
{
method: 'GET',
headers: {
Authorization: config.TOKEN,
},
}
).then (result => {
return result.text ();
});
};
var refreshTokens = function () {
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);
} else {
parsedResponse = JSON.parse (body);
if (parsedResponse.refresh_token) {
databaseHelper
.updateTokens (
parsedResponse.refresh_token,
parsedResponse.access_token,
parsedResponse.expires_in
)
.then (() => {
resolve ();
})
.catch (e => {
reject (e);
});
} else {
reject (body);
}
}
});
});
};
var generateInteractionModel = function (skill) {
let result = {};
let allIntents = [];
skill.intents.map (intent => {
allIntents.push ({name: intent.intentName, samples: intent.questions});
});
//Built-In like intents (Amazon built-in don't work, probably something related to existance of dialog intent
allIntents.push ({
name: 'HelpIntent',
samples: ['Help', 'Can you help me', 'I need help'],
slots: [],
},
{
name: 'CancelIntent',
samples: ['Cancel', 'Stop', 'Please stop'],
slots: [],
},
{
name: 'YesIntent',
samples: [
'Yes',
'Yes please',
'I would like that',
'Yes I would like that',
],
slots: [],
},
{
name: 'NoIntent',
samples: ['No', 'No thank you'],
slots: [],
});
//Special intent for sending message (Dialog)
allIntents.push ({
name: 'SendMessageIntent',
samples: [
'I would like to send a message',
'I want to send a message',
'Send message',
],
slots: [
{
name: 'Name',
type: 'AMAZON.US_FIRST_NAME',
samples: ['My name is {Name}', 'I am {Name}', '{Name}'],
},
{
name: 'Email',
type: 'EmailSlot',
samples: ['My email is {Email}', '{Email}'],
},
{
name: 'Message',
type: 'MessageSlot',
samples: ['{Message}'],
},
],
});
let customSlotTypes = [
{
name: 'EmailSlot',
values: [
{
id: null,
name: {
value: 'bla@bla.bla',
synonyms: [],
},
},
{
id: null,
name: {
value: 'bla.bla@bla.bla.bla',
synonyms: [],
},
},
{
id: null,
name: {
value: 'bla_bla@bla.bla',
synonyms: [],
},
},
],
},
{
name: 'MessageSlot',
values: [
{
id: null,
name: {
value: 'Quick brown fox jumps over lazy dog',
synonyms: [],
},
},
{
id: null,
name: {
value: 'Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog.',
synonyms: [],
},
},
{
id: null,
name: {
value: 'Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog.',
synonyms: [],
},
},
],
},
];
let dialogPrompts = [
{
id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Name',
variations: [
{
type: 'PlainText',
value: 'What is your name ?',
},
{
type: 'PlainText',
value: 'Tell me your name',
},
],
},
{
id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Email',
variations: [
{
type: 'PlainText',
value: 'What is your email ?',
},
{
type: 'PlainText',
value: 'Tell me your email',
},
],
},
{
id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Message',
variations: [
{
type: 'PlainText',
value: 'What is your message',
},
],
},
];
let dialogIntents = [
{
name: 'SendMessageIntent',
confirmationRequired: false,
prompts: {},
slots: [
{
name: 'Name',
type: 'AMAZON.US_FIRST_NAME',
elicitationRequired: true,
confirmationRequired: false,
prompts: {
elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Name',
},
},
{
name: 'Email',
type: 'EmailSlot',
elicitationRequired: true,
confirmationRequired: false,
prompts: {
elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Email',
},
},
{
name: 'Message',
type: 'MessageSlot',
elicitationRequired: true,
confirmationRequired: false,
prompts: {
elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Message',
},
},
],
},
];
result.interactionModel = {};
result.interactionModel.languageModel = {
invocationName: skill.invocationName,
types: customSlotTypes,
intents: allIntents,
};
result.interactionModel.prompts = dialogPrompts;
result.interactionModel.dialog = {};
result.interactionModel.dialog.intents = dialogIntents;
return JSON.stringify (result);
};
var uploadSkill = function (skill) {
let generatedInteractionModel = generateInteractionModel (skill);
console.log(skill.skillID);
return fetch (
`https://api.amazonalexa.com/v1/skills/${skill.skillID}/stages/development/interactionModel/locales/en-US`,
{
method: 'PUT',
headers: {
Authorization: config.TOKEN,
},
body: generatedInteractionModel,
}
);
};
module.exports = {
updateSkill: function (skill) {
return new Promise ((resolve, reject) => {
if (new Date () / 1000 > config.TOKEN_EXPIRES_IN) {
refreshTokens ()
.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,99 @@
const config = require ('../config/config');
var ObjectID = require ('mongodb').ObjectID;
var db = null;
module.exports = {
initModule: function (databaseObject) {
db = databaseObject;
db.collection ('intent_list');
},
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);
});
},
updateTokens: function (refresh_token, access_token, expires_in) {
return new Promise ((resolve, reject) => {
let newTokenDocument = {
id: 1,
refresh_token: refresh_token,
access_token: access_token,
expires_in: new Date () / 1000 + expires_in,
};
db
.collection ('token_list')
.update ({id: 1}, newTokenDocument, {upsert: true}, (err, result) => {
if (err) {
reject (err)
}else{
config.REFRESH_TOKEN = refresh_token;
config.TOKEN = access_token;
config.TOKEN_EXPIRES_IN = newTokenDocument.expires_in;
resolve ();
}
});
});
},
getSkill: function (skillDbID) {
return new Promise ((resolve, reject) => {
db
.collection ('skill_list')
.findOne ({_id: ObjectID (skillDbID)}, (err, skill) => {
if (skill) {
resolve (skill);
} else {
reject (err);
}
});
});
},
updateSkill: function (id, skill) {
return new Promise ((resolve, reject) => {
db
.collection ('skill_list')
.update ({_id: ObjectID (id)}, skill, {upsert: true}, (err, result) => {
if (err){
reject();
}else{
resolve();
}
});
});
},
deleteSkill: function (id) {
return new Promise ((resolve, reject) => {
db
.collection ('skill_list')
.remove ({_id: ObjectID (id)}, (err, result) => {
if (err){
reject (err);
}else{
resolve (result);
}
});
});
}
};

75
backend/helpers/email.js Normal file
View File

@@ -0,0 +1,75 @@
const nodemailer = require ('nodemailer');
const emailConfig = require('../config/email');
module.exports = {
transformEmailFromAlexaResponse: function (email) {
//email from alexa response will contain words instead of symbols, like :
//at = @
//underscore = _
//dash = -
//dot = .
//TODO: This list should be longer
let transformedEmail = email
.replace (/\s/g, '') //remove all spaces
.replace (/at/gi, '@')
.replace (/underscore/gi, '_')
.replace (/dash/gi, '-')
.replace (/dot/gi, '.');
return transformedEmail;
},
isEmailValid: function (email) {
console.log ('Email to validate : ' + email);
let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return validEmailRegex.test (email);
},
sendEmail: function (name, fromEmail, message, toEmail) {
return new Promise ((resolve, reject) => {
fromEmail = this.transformEmailFromAlexaResponse(fromEmail);
let messageBody =
'Hello. User left you a message on Saburly service using Alexa skill. \r\nMessage : ' +
message +
'\r\nName : ' +
name +
'\r\nEmail : ' +
fromEmail +
'\r\nYour Saburly team';
let messageBodyHTML =
'<p>Hello. User left you a message on Saburly service using Alexa skill.</p><br/><b>Message : </b><br/><p>' +
message +
'</p><br/><b>Name : </b>' +
name +
'<br/><b>Email : </b>' +
fromEmail +
'<br/><br/><b>Your Saburly team</b>';
let transporter = nodemailer.createTransport ({
host: emailConfig.SMTP_HOST,
port: emailConfig.PORT,
secure: emailConfig.SECURE,
auth: emailConfig.AUTH,
});
var mailOptions = {
from: emailConfig.FROM_EMAIL,
replyTo: fromEmail,
to: toEmail,
subject: emailConfig.SUBJECT,
text: messageBody,
html: messageBodyHTML,
};
transporter.sendMail (mailOptions, (error, info) => {
if (error) {
reject (error);
} else {
resolve (info);
}
});
});
},
};

View File

@@ -0,0 +1,112 @@
let request = require ('request');
let Parser = require ('rss-parser');
let summarizer = require ('nodejs-text-summarizer');
var htmlToText = require ('html-to-text');
const constants = require ('../config/constants');
let parser = new Parser ();
getDataFromWPJSON = function (sourceUrl, page = 1, maxPosts = 10) {
return new Promise ((resolve, reject) => {
var options = {
method: 'GET',
url: `${sourceUrl}/wp-json/wp/v2/posts`,
qs: {
page: page,
per_page: maxPosts,
},
};
request (options, (error, response, body) => {
if (error) {
reject (error);
} else {
resolve (JSON.parse (body));
}
});
});
};
summarizeText = function (text, length, clearText = true) {
let preparedText = text;
if (clearText) {
preparedText = htmlToText.fromString (text, {
wordwrap: false,
ignoreHref: true,
ignoreImage: true,
});
}
return summarizer (preparedText, {n: length});
};
getTitlesFromWP = function (sourceUrl) {
return new Promise ((resolve, reject) => {
getDataFromWPJSON (sourceUrl)
.then (rawData => {
let result = '';
rawData.forEach (post => {
result +=
post.title.rendered +
`<break time="${constants.voiceResponseTimings.PAUSE_BETWEEN_TITLES}ms"/> `;
});
resolve (result);
})
.catch (err => {
reject (constants.voiceResponseStrings.ERROR_FETCHING_CONTENT);
});
});
};
getLatestNewsFromWP = function (
sourceUrl,
postCount = 10,
includeTitle = false
) {
return new Promise ((resolve, reject) => {
getDataFromWPJSON (sourceUrl, 1, postCount)
.then (rawData => {
let result = '';
let htmlToTextOptions = {
wordwrap: false,
ignoreHref: true,
ignoreImage: true,
};
try {
rawData.forEach (post => {
result += includeTitle ? post.title.rendered : '';
result += includeTitle
? `<break time="${constants.voiceResponseTimings.PAUSE_BETWEEN_TITLE_AND_CONTENT}ms"/>`
: '';
result += summarizeText (
post.content.rendered,
constants.FIXED_SUMMARY_LENGTH
);
result += `<break time="${constants.voiceResponseTimings.PAUSE_BETWEEN_NEWS}ms"/>`;
});
resolve (result);
} catch (err) {
reject (constants.voiceResponseStrings.ERROR_SUMMARIZING_CONTENT);
}
})
.catch (err => {
reject (constants.voiceResponseStrings.ERROR_FETCHING_CONTENT);
});
});
};
module.exports = {
getAnswerFromWP: function (sourceUrl, contentType) {
//This function will extract needed data from JSON, which we got from getDataFromWPJSON
switch (contentType) {
case constants.contentType.TITLES:
return getTitlesFromWP (sourceUrl);
break;
case constants.contentType.NEWS:
return getLatestNewsFromWP (sourceUrl);
break;
}
},
};

View File

@@ -0,0 +1,107 @@
const constants = require ('../config/constants');
validateEmail = function (email) {
if (email.length > constants.stringConstraints.EMAIL_MAX_LENGTH) return false;
let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return validEmailRegex.test (email);
};
validateIntentName = function (intentName) {
if (
intentName.length < constants.stringConstraints.INTENT_NAME_MIN_LENGTH ||
intentName.length > constants.stringConstraints.INTENT_NAME_MAX_LENGTH
)
return false;
let validIntentNameRegex = /^[a-z]*$/i;
return validIntentNameRegex.test (intentName);
};
validateQuestion = function (question) {
if (
question.length < constants.stringConstraints.QUESTION_MIN_LENGTH ||
question.length > constants.stringConstraints.QUESTION_MAX_LENGTH
)
return false;
let validQuestionNameRegex = /^[a-z,.' ]*$/i;
return validQuestionNameRegex.test (question);
};
validateAnswer = function (answer, answerType) {
if (answerType !== constants.answerType.PREDEFINED) return true;
if (
answer.length < constants.stringConstraints.ANSWER_MIN_LENGTH ||
answer.length > constants.stringConstraints.ANSWER_MAX_LENGTH
)
return false;
let validAnswerRegex = /^[a-z,.' ]*$/i;
return validAnswerRegex.test (answer);
};
validateExternalAnswerSource = function (externalAnswerSource, answerType){
// TODO: implement validation logic
return true;
}
validateInvocationName = function (invocationName) {
if (
invocationName.length < constants.stringConstraints.INVOCATION_NAME_MIN_LENGTH ||
invocationName.length > constants.stringConstraints.INVOCATION_NAME_MAX_LENGTH
)
return false;
let validInvocationNameRegex = /^[a-z,.' ]*$/;
return validInvocationNameRegex.test (invocationName);
};
validateInvocationAnswer = function (invocationAnswer) {
if (invocationAnswer.length > constants.stringConstraints.INVOCATION_ANSWER_MAX_LENGTH)
return false;
let validInvocationAnswerRegex = /^[a-z,.' ]*$/i;
return validInvocationAnswerRegex.test (invocationAnswer);
};
validateIntentExplanation = function (explanation) {
if (explanation.length > constants.stringConstraints.INTENT_EXPLANATION_MAX_LENGTH)
return false;
let validExplanationRegex = /^[a-z,.' ]*$/i;
return validExplanationRegex.test (explanation);
};
module.exports = {
validateSkill: function (skill) {
try {
if (
!validateEmail (skill.contactEmail) ||
!validateInvocationName (skill.invocationName) ||
!validateInvocationAnswer (skill.invocationAnswer)
)
return false;
for (let i = 0; i < skill.intents.length; i++) {
if (!validateIntentName (skill.intents[i].intentName)) return false;
if (!validateAnswer (skill.intents[i].answer, skill.intents[i].answerType)) return false;
if (!validateExternalAnswerSource(skill.intents[i].externalAnswerSource, skill.intents[i].answerType)) return false;
for (let j = 0; j < skill.intents.length; j++) {
if (i === j) continue;
if (skill.intents[i].intentName === skill.intents[j].intentName)
return false;
}
for (let j = 0; j < skill.intents[i].questions.length; j++) {
if (!validateQuestion (skill.intents[i].questions[j])) return false;
for (let k = 0; k < skill.intents[i].questions.length; k++) {
if (j === k) continue;
if (skill.intents[i].questions[j] === skill.intents[i].questions[k])
return false;
}
}
}
return true;
} catch (e) {
console.log ('Error : ' + e);
return false;
}
},
};

View File

@@ -0,0 +1,7 @@
module.exports = 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, PUT');
res.header ('Access-Control-Allow-Credentials', 'true');
next ();
};

291
backend/models/alexa.js Normal file
View File

@@ -0,0 +1,291 @@
var Alexa = require ('alexa-sdk');
const config = require ('../config/config');
var databaseHelper = require ('../helpers/database');
var emailHelper = require ('../helpers/email');
const constants = require ('../config/constants');
let predefinedSourceHelper = require ('../helpers/externalSource');
var handlers = {};
var destinationEmail;
let skillName;
module.exports = {
run: function (req, res) {
// Build the context manually, because Amazon Lambda is missing
var context = {
succeed: function (result) {
res.json (result);
},
fail: function (error) {
console.log (error);
//We could send error json from here
},
};
var alexa = Alexa.handler (req.body, context);
alexa.appId = config.SKILL_ID;
alexa.registerHandlers (handlers);
alexa.execute ();
},
updateModel: function () {
//Get info from database, and store it for faster response on intent
databaseHelper
.getSkill (config.SKILL_DB_ID)
.then (activeSkill => {
handlers = {};
destinationEmail = activeSkill.contactEmail;
skillName = activeSkill.invocationName;
let listOfPossibleQuestions = '';
activeSkill.intents.forEach (intent => {
if (intent.questions.length > 0 && intent.intentExplanation) {
listOfPossibleQuestions +=
intent.intentExplanation +
intent.questions[0] +
'<break time="' +
constants.voiceResponseTimings.PAUSE_BETWEEN_QUESTIONS +
'ms"/>';
}
});
listOfPossibleQuestions +=
'If you dont know what to do, just say help or stop';
//Handler for launch requestconsole.log()
handlers = {
LaunchRequest: function () {
this.response
.speak (
activeSkill.invocationAnswer +
'<break time="' +
constants.voiceResponseTimings.PAUSE_AFTER_WELCOME_MESSAGE +
'ms"/>' +
'Would you like to hear list of questions that you can ask me'
)
.listen (constants.voiceResponseStrings.GENERIC_CONTINUE);
this.attributes['LaunchRequestYesNo'] = true;
this.emit (':responseReady');
},
};
//Handlers for user defined questions
activeSkill.intents.map (intent => {
handlers[intent.intentName] = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
}
let answerPromiseProps = {
resolve: null,
reject: null,
};
let answerPromise = new Promise ((resolve, reject) => {
answerPromiseProps = {
resolve: resolve,
reject: reject,
};
});
switch (intent.answerType) {
case constants.answerType.PREDEFINED:
answerPromiseProps.resolve (intent.answer);
break;
case constants.answerType.EXTERNAL_SOURCE_WP_TITLES:
predefinedSourceHelper
.getAnswerFromWP (
intent.externalAnswerSource,
constants.contentType.TITLES
)
.then (answer => {
answerPromiseProps.resolve (answer);
})
.catch (error => {
answerPromiseProps.reject (error);
});
break;
case constants.answerType.EXTERNAL_SOURCE_WP_NEWS:
predefinedSourceHelper
.getAnswerFromWP (
intent.externalAnswerSource,
constants.contentType.NEWS
)
.then (answer => {
answerPromiseProps.resolve (answer);
})
.catch (error => {
answerPromiseProps.reject (error);
});
break;
}
answerPromise
.then (answer => {
this.response
.speak (answer);
this.emit (':responseReady');
})
.catch (error => {
this.response
.speak (error);
this.emit (':responseReady');
});
};
});
//Handler for sending message
handlers.SendMessageIntent = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
}
let intent = this.event.request.intent;
console.log ('Dialog state : ' + this.event.request.dialogState);
console.log (intent);
//STARTED, IN_PROGRESS
if (!intent.slots.Name.value) {
//Name not defined yet, ask user for name
const slotToElicit = 'Name';
const speechOutput = 'What is your name';
const repromptSpeech = speechOutput;
this.emit (
':elicitSlot',
slotToElicit,
speechOutput,
repromptSpeech
);
} else if (!intent.slots.Email.value) {
//Name not defined yet, ask user for email
const slotToElicit = 'Email';
const speechOutput =
'Ok ' + intent.slots.Name.value + '. What is your email';
const repromptSpeech = speechOutput;
this.emit (
':elicitSlot',
slotToElicit,
speechOutput,
repromptSpeech
);
} else if (!intent.slots.Message.value) {
intent.slots.Email.value = emailHelper.transformEmailFromAlexaResponse (
intent.slots.Email.value
);
if (!emailHelper.isEmailValid (intent.slots.Email.value)) {
//Email is not valid, ask again
const slotToElicit = 'Email';
const speechOutput =
'Sorry, that was not valid email. What is your email';
const repromptSpeech = speechOutput;
this.emit (
':elicitSlot',
slotToElicit,
speechOutput,
repromptSpeech
);
} else {
//Email is valid
const slotToElicit = 'Message';
const speechOutput = 'Great. What is your message';
const repromptSpeech = speechOutput;
this.emit (
':elicitSlot',
slotToElicit,
speechOutput,
repromptSpeech
);
}
} else {
//all slots are filled
console.log ('Name : ' + intent.slots.Name.value);
console.log ('Email : ' + intent.slots.Email.value);
console.log ('Message : ' + intent.slots.Message.value);
emailHelper
.sendEmail (
intent.slots.Name.value,
intent.slots.Email.value,
intent.slots.Message.value,
destinationEmail
)
.then (info => {
console.log (info);
this.response.speak (
'Ok. Message sent. Someone will contact you ASAP'
);
this.emit (':responseReady');
})
.catch (error => {
console.log (error);
this.response.speak (
'Sorry, there was a problem with sending message.'
);
this.emit (':responseReady');
});
}
};
//Built-In intents
handlers.CancelIntent = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
}
this.response.speak (`Thank you for using ${skillName}`);
this.emit (':responseReady');
};
handlers.HelpIntent = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
}
this.response
.speak (listOfPossibleQuestions)
.listen (constants.voiceResponseStrings.GENERIC_CONTINUE);
this.emit (':responseReady');
};
handlers.YesIntent = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
this.emit ('HelpIntent');
} else {
this.response
.speak (constants.voiceResponseStrings.DIDNT_ASK_ANYTHING);
this.emit (':responseReady');
}
};
handlers.NoIntent = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
this.response
.speak ('');
this.emit (':responseReady');
} else {
this.response
.speak (constants.voiceResponseStrings.DIDNT_ASK_ANYTHING);
this.emit (':responseReady');
}
};
//Default handler for unknown question
handlers.Unhandled = function () {
this.response
.speak (constants.voiceResponseStrings.QUESTION_NOT_FOUND);
this.emit (':responseReady');
};
//Session handlers
handlers.SessionEndedRequest = function () {
this.response.speak (`Thank you for using ${skillName}`);
this.emit (':responseReady');
};
})
.catch (e => {
//Something is wrong, skill is not ready, use catch-all intent to inform user
console.log ('Error. Skill doesnt exist');
});
},
};

File diff suppressed because it is too large Load Diff

20
backend/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "tellall",
"version": "1.0.0",
"description": "",
"main": "test.js",
"dependencies": {
"alexa-sdk": "^1.0.25",
"alexa-verifier-middleware": "^1.0.1",
"body-parser": "^1.13.1",
"ejs": "^2.5.7",
"express": "^4.13.0",
"html-to-text": "^4.0.0",
"isomorphic-fetch": "^2.2.1",
"mongodb": "^2.2.33",
"nodejs-text-summarizer": "GotPPay/nodejs-text-summarizer",
"nodemailer": "^4.4.1",
"request": "^2.83.0",
"rss-parser": "^3.1.1"
}
}

28
backend/server.js Normal file
View File

@@ -0,0 +1,28 @@
var databaseHelper = require ('./helpers/database');
const config = require ('./config/config');
var express = require ('express');
var alexa = require ('./models/alexa');
var MongoClient = require ('mongodb').MongoClient;
var ObjectID = require ('mongodb').ObjectID;
const router = express.Router ();
var app = express ();
app.set ('view engine', 'ejs'); // Should be removed
app.use (require ('./middleware')); //common middleware for all requests
app.use (require ('./controllers')); //all routes
MongoClient.connect (config.DB_URL)
.then (database => {
databaseHelper.initModule (database);
app.listen (config.PORT, () => {
console.log ('Express server running on port ' + config.PORT);
alexa.updateModel ();
databaseHelper.loadTokens ();
});
})
.catch (e => {
console.log ('error : ' + e);
});

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,14 +0,0 @@
{
"name": "example",
"version": "1.0.0",
"description": "",
"main": "test.js",
"dependencies": {
"body-parser": "^1.13.1",
"ejs": "^2.3.1",
"express": "^4.13.0",
"alexa-app": "4.2.0"
},
"author": "Matt Kruse <github@mattkruse.com> (http://mattkruse.com/)",
"license": "MIT"
}

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));
});

View File

@@ -1,10 +0,0 @@
<div style="white-space:pre;border:1px solid black;margin:5px;padding:5px;font-family:monospace;">
Schema:
<%=schema%>
Utterances:
<%=utterances%>
</div>

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

12961
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
web/package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "web",
"version": "0.1.0",
"private": true,
"dependencies": {
"keymaster": "^1.6.2",
"node-sass-chokidar": "0.0.3",
"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",
"node-sass": "^4.7.2",
"npm-run-all": "^4.1.2"
},
"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",
"react-build": "react-scripts build",
"build": "npm-run-all -p build-css react-build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {
"babel-jest": "^22.4.3",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-to-json": "^3.3.3",
"jest": "^20.0.3",
"jest-enzyme": "^6.0.0",
"nodemon": "^1.12.1",
"react-test-renderer": "^16.3.0"
}
}

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"
}

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

@@ -0,0 +1,372 @@
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 Contact from './components/Contact';
import Popup from 'react-popup';
import {getSkill, updateSkill} from './lib/api';
import {isEmailValid} from './lib/helpers';
import {
NEW_INTENT_SELECTED_INDEX,
LAUNCH_REQUEST_SELECTED_INDEX,
CONTACT_SELECTED_INDEX,
RESULT_CODES,
INVOCATION_NAME_MIN_LENGTH,
INTENT_NAME_MIN_LENGTH,
QUESTION_MIN_LENGTH,
ANSWER_MIN_LENGTH,
ANSWER_TYPE,
} from './config/constants';
class App extends Component {
constructor (props) {
super (props);
this.state = {
_id: '5abd461329f85e4ec728d945',
skillID: '',
skillName: '',
invocationName: 'Saburly',
invocationAnswer: 'We are saburly',
allIntents: [],
selectedIntent: {
intentName: '',
intentExplanation: '',
questions: [''],
answer: '',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
},
selectedIndex: NEW_INTENT_SELECTED_INDEX,
contactEmail: '',
waiting: false,
};
getSkill (this.state._id).then (l => l.json ()).then (result => {
if (result === undefined) return;
this.setState ({
skillID: result.skillID,
skillName: result.skillName,
invocationName: result.invocationName,
invocationAnswer: result.invocationAnswer,
allIntents: result.intents,
contactEmail: result.contactEmail,
});
});
this.handleIntentClick = this.handleIntentClick.bind (this);
this.handleLaunchRequestClick = this.handleLaunchRequestClick.bind (this);
this.handleDeleteIntentClick = this.handleDeleteIntentClick.bind (this);
this.handleSaveIntentClick = this.handleSaveIntentClick.bind (this);
this.handleAddIntentClick = this.handleAddIntentClick.bind (this);
this.handleSaveLaunchRequestClick = this.handleSaveLaunchRequestClick.bind (
this
);
this.createSkill = this.createSkill.bind (this);
this.sendSkill = this.sendSkill.bind (this);
this.handleContactClick = this.handleContactClick.bind (this);
this.handleSaveEmailClick = this.handleSaveEmailClick.bind (this);
}
render () {
let rightPanel;
switch (this.state.selectedIndex) {
case LAUNCH_REQUEST_SELECTED_INDEX:
rightPanel = (
<LaunchRequest
invocationName={this.state.invocationName}
invocationAnswer={this.state.invocationAnswer}
onSaveClick={this.handleSaveLaunchRequestClick}
waiting={this.state.waiting}
/>
);
break;
case CONTACT_SELECTED_INDEX:
rightPanel = (
<Contact
contactEmail={this.state.contactEmail}
onSaveEmailClick={this.handleSaveEmailClick}
waiting={this.state.waiting}
/>
);
break;
default:
rightPanel = (
<IntentDetails
selectedIntent={this.state.selectedIntent}
onDeleteIntentClick={this.handleDeleteIntentClick}
onSaveIntentClick={this.handleSaveIntentClick}
waiting={this.state.waiting}
/>
);
}
return (
<div className="App">
<Popup />
<div className="App-header">
<h1> Tell All </h1>
</div>
<IntentList
allIntents={this.state.allIntents}
onLaunchRequestClick={this.handleLaunchRequestClick}
onContactClick={this.handleContactClick}
onIntentClick={this.handleIntentClick}
onAddIntentClick={this.handleAddIntentClick}
selectedIndex={this.state.selectedIndex}
waiting={this.state.waiting}
/>
{rightPanel}
</div>
);
}
createSkill (intents, name, answer, email, updateOnAmazon) {
return {
_id: this.state._id,
skillID: this.state.skillID,
intents: intents,
invocationName: name,
invocationAnswer: answer,
contactEmail: email,
updateOnAmazon: updateOnAmazon,
};
}
handleIntentClick (selectedIntent, index) {
this.setState ({
selectedIntent: selectedIntent,
selectedIndex: index,
});
}
handleLaunchRequestClick () {
this.setState ({selectedIndex: LAUNCH_REQUEST_SELECTED_INDEX});
}
handleContactClick () {
this.setState ({selectedIndex: CONTACT_SELECTED_INDEX});
}
handleSaveLaunchRequestClick (name, answer) {
if (name.length < INVOCATION_NAME_MIN_LENGTH) {
Popup.alert ('Invocation name should be at least 2 characters long');
return;
}
this.setState ({
waiting: true,
invocationName: name,
invocationAnswer: answer,
});
this.sendSkill (
this.state.allIntents,
true,
{waiting: false},
{waiting: false},
name,
answer,
this.state.contactEmail,
true
);
}
handleSaveEmailClick (email) {
if (isEmailValid (email)) {
this.setState ({waiting: true});
this.sendSkill (
this.state.allIntents,
true,
{contactEmail: email, waiting: false},
{waiting: false},
this.state.invocationName,
this.state.invocationAnswer,
email,
false
);
} else {
Popup.alert ('Please enter valid email');
}
}
handleDeleteIntentClick (selectedIntent) {
let id = -1;
//TODO : Change comparsion method ! Same object with different proeprty sorting will not be same string
this.state.allIntents.map ((intent, index) => {
if (
id === -1 &&
JSON.stringify (selectedIntent) === JSON.stringify (intent)
)
id = index;
});
if (id !== -1) {
try {
let newAllIntentsJSON = JSON.stringify (this.state.allIntents);
let newAllIntents = JSON.parse (newAllIntentsJSON);
newAllIntents.splice (id, 1);
this.setState ({waiting: true});
let newState = {
allIntents: newAllIntents,
selectedIntent: {intentName: '', questions: [''], answer: ''},
waiting: false,
};
this.sendSkill (
newAllIntents,
true,
newState,
{waiting: false},
this.state.invocationName,
this.state.invocationAnswer,
this.state.contactEmail,
true
);
} catch (e) {
console.log ('error : ' + e);
}
}
}
handleSaveIntentClick (selectedIntent) {
if (selectedIntent.intentName.length < INTENT_NAME_MIN_LENGTH) {
Popup.alert ('Question name should have at least 2 characters');
return;
}
if (
selectedIntent.answerType === ANSWER_TYPE.PREDEFINED &&
selectedIntent.answer.length < ANSWER_MIN_LENGTH
) {
Popup.alert ('Answer should have at least 2 characters');
return;
}
for (let i = 0; i < selectedIntent.questions.length; i++) {
if (selectedIntent.questions[i].length < QUESTION_MIN_LENGTH) {
Popup.alert ('Question variant should have at least 2 characters');
return;
}
}
//Check for same question variants and same question name in other intents
//all intents with the same intentName, or some of the questions are the same
//will be kept in filteredIntents. After filterring, there should be only one
//intent left, the selected one
let selectedIntentQuestionsForSearch = selectedIntent.questions.map(question=>
question.toLowerCase().trim());
let filteredIntents = this.state.allIntents.filter(intent=>{
let result = (selectedIntent.intentName.toLowerCase().trim() === intent.intentName.toLowerCase().trim());
let filteredQuestions = intent.questions.filter(question=>{
return (selectedIntentQuestionsForSearch.indexOf(question.toLowerCase().trim())!==-1);
});
return (result || filteredQuestions.length > 0);
});
if (filteredIntents.length > 1){
Popup.alert('Question name or question variant already exists');
return;
}
let newAllIntentsJSON = JSON.stringify (this.state.allIntents);
let newAllIntents = JSON.parse (newAllIntentsJSON);
let resolveState = null;
let rejectState = {waiting: false};
if (this.state.selectedIndex === NEW_INTENT_SELECTED_INDEX) {
//new intent
newAllIntents.push (selectedIntent);
resolveState = {
allIntents: newAllIntents,
selectedIntent: selectedIntent,
selectedIndex: newAllIntents.length - 1,
waiting: false,
};
} else {
newAllIntents[this.state.selectedIndex] = selectedIntent;
resolveState = {
allIntents: newAllIntents,
selectedIntent: selectedIntent,
waiting: false,
};
}
this.setState ({waiting: true});
this.sendSkill (
newAllIntents,
true,
resolveState,
rejectState,
this.state.invocationName,
this.state.invocationAnswer,
this.state.contactEmail,
true
);
}
handleAddIntentClick () {
this.setState ({
allIntents: this.state.allIntents,
selectedIndex: NEW_INTENT_SELECTED_INDEX,
launchRequest: false,
selectedIntent: {
intentName: '',
questions: [''],
answer: '',
intentExplanation: '',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
},
});
}
sendSkill (
newAllIntents,
showPopUp,
resolveState,
rejectState,
newName,
newAnswer,
email,
updateOnAmazon
) {
return new Promise ((resolve, reject) => {
updateSkill (
this.createSkill (
newAllIntents,
newName,
newAnswer,
email,
updateOnAmazon
)
)
.then (l => l.json ())
.then (result => {
if (result.result !== RESULT_CODES.OK) {
console.log (result);
if (showPopUp)
Popup.alert ('Model was not saved. Please try again');
this.setState (rejectState);
//reject('Error code : ' + jResult.result);
} else {
if (showPopUp) Popup.alert ('Saved');
this.setState (resolveState);
resolve ();
}
})
.catch (e => {
console.log ('error : ' + e);
if (showPopUp) Popup.alert ('Model was not saved. Please try again');
this.setState (rejectState);
//reject(e);
});
});
}
}
export default App;

View File

@@ -0,0 +1,60 @@
import React, {Component} from 'react';
import {Button} from 'react-md';
import AnswerSourceForm from './helper/AnswerSourceForm';
import '../css/components/IntentDetails.css';
class AnswerSource extends Component {
constructor (props) {
super (props);
this.state = {
isModalOpen: false,
answerType: this.props.answerType
};
}
onOpen () {
this.setState ({
isModalOpen: true,
answerType: this.props.answerType
});
}
onClose () {
this.setState ({
isModalOpen: false,
});
}
onSave(){
this.onClose();
this.props.onSaveAnswerType(this.state.answerType);
}
onSourceChange(value, event){
this.setState({answerType:parseInt(value,10)});
}
render () {
let modal;
if (this.state.isModalOpen) {
modal = <AnswerSourceForm
isModalOpen={this.state.isModalOpen}
answerType={this.state.answerType}
onSave={this.onSave.bind(this)}
onClose={this.onClose.bind(this)}
onSourceChange={this.onSourceChange.bind(this)}
/>
}
return (
<div>
<Button flat primary onClick={this.onOpen.bind (this)}>
Answer type
</Button>
{modal}
</div>
);
}
}
export default AnswerSource;

View File

@@ -0,0 +1,50 @@
import React, { Component } from 'react';
import {Button, TextField} from 'react-md';
import '../css/Common.css';
import '../css/components/Contact.css';
import {EMAIL_MAX_LENGTH} from '../config/constants';
class Contact extends Component {
constructor(props){
super(props);
this.state = {contactEmail: props.contactEmail};
this.handleEmailEdit = this.handleEmailEdit.bind(this);
}
componentWillReceiveProps(props){
this.setState({contactEmail: props.contactEmail});
}
render() {
return (
<div className="RightPanelBox">
<h5 className="PanelSubTitle"> Contact address will be used for direct messaging through Alexa </h5>
<TextField
id="contact email"
lineDirection="center"
label="Contact email"
className="md-cell md-cell--bottom ContactEmailInput"
maxLength={EMAIL_MAX_LENGTH}
onChange={this.handleEmailEdit}
value={this.state.contactEmail}/>
<br></br>
<br></br>
<br></br>
<Button className="SaveButton" flat primary swapTheming
onClick={()=>{this.props.onSaveEmailClick(this.state.contactEmail)}}
disabled={this.props.waiting}>Save</Button>
</div>
);
}
handleEmailEdit(e){
const isEmailValid = e.length < EMAIL_MAX_LENGTH;
if (isEmailValid){
this.setState({contactEmail: e});
}
}
}
export default Contact;

View File

@@ -0,0 +1,209 @@
import React, {Component} from 'react';
import {Button, SVGIcon, TextField} from 'react-md';
import AnswerSource from './AnswerSource.js';
import '../css/components/IntentDetails.css';
import '../css/Common.css';
import {
QUESTION_MAX_LENGTH,
ANSWER_MAX_LENGTH,
INTENT_NAME_MAX_LENGTH,
INTENT_EXPLANATION_MAX_LENGTH,
ANSWER_TYPE,
} from '../config/constants';
import AnswerTextBox from './helper/AnswerTextBox.js';
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.handleIntentNameEdit = this.handleIntentNameEdit.bind (this);
this.handleIntentExplanationEdit = this.handleIntentExplanationEdit.bind (
this
);
}
componentWillReceiveProps (props) {
this.setState ({intent: props.selectedIntent});
}
render () {
return (
<div className="RightPanelBox">
<div className="QuestionBox">
<h5 className="PanelSubTitle">
{' '}
In introduction, Alexa will help users to ask her the right questions about your business. For Example, she will say : "To ask us about our services, say : What do you do ? ". What do you do ? is defined in question field. Alexa will use first variation of question in intro.
</h5>
<TextField
id="intent explanation"
lineDirection="center"
placeholder="To ask us about our services, say "
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
onChange={this.handleIntentExplanationEdit}
maxLength={INTENT_EXPLANATION_MAX_LENGTH}
value={this.state.intent.intentExplanation}
/>
<br />
<TextField
id="intent name"
lineDirection="center"
label="Question name"
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
onChange={this.handleIntentNameEdit}
maxLength={INTENT_NAME_MAX_LENGTH}
value={this.state.intent.intentName}
/>
</div>
<h5 className="QuestionTitle">Question variants</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 IntentDetailsInputBoxes"
maxLength={QUESTION_MAX_LENGTH}
rightIcon={
<SVGIcon
onClick={() => {
this.deleteQuestion (index);
}}
>
{' '}
<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>
);
})}
<Button
className="AddQuestionVariantButton"
icon
primary
onClick={this.addQuestion}
disabled={this.props.waiting}
>
add
</Button>
<AnswerSource
className="AnswerTypeButton"
onSaveAnswerType={this.handleExternalSourceSave.bind(this)}
answerType={this.state.intent.answerType}
/>
<AnswerTextBox
answerType={this.state.intent.answerType}
externalAnswerSource={this.state.intent.externalAnswerSource}
handleAnswerSourceEdit={this.handleAnswerSourceEdit.bind(this)}
handleAnswerEdit={this.handleAnswerEdit.bind(this)}
answer={this.state.intent.answer}
/>
<Button
className="IntentDetailsButton-firstInRow"
flat
primary
swapTheming
onClick={() => {
this.props.onSaveIntentClick (this.state.intent);
}}
disabled={this.props.waiting}
>
Save
</Button>
<Button
className="IntentDetailsButton"
flat
primary
onClick={() => {
this.props.onDeleteIntentClick (this.state.intent);
}}
disabled={this.props.waiting}
>
Delete
</Button>
</div>
);
}
addQuestion () {
let newIntent = this.state.intent;
newIntent.questions.push ('');
this.setState ({intent: newIntent});
}
deleteQuestion (index) {
if (this.state.intent.questions.length > 1) {
let newIntent = this.state.intent;
if (index >= 0 && index < newIntent.questions.length) newIntent.questions.splice (index, 1);
this.setState ({intent: newIntent});
}
}
handleQuestionEdit (e, index) {
const isQuestionValid = e.length < QUESTION_MAX_LENGTH && /^[a-z,.' ]*$/i.test (e);
if (isQuestionValid){
let newIntent = this.state.intent;
newIntent.questions[index] = e;
this.setState ({intent: newIntent});
}
}
handleIntentExplanationEdit (e, index) {
const isExplanationValid = e.length < INTENT_EXPLANATION_MAX_LENGTH && /^[a-z,.' ]*$/i.test (e);
if (isExplanationValid){
let newIntent = this.state.intent;
newIntent.intentExplanation = e;
this.setState ({intent: newIntent});
}
}
handleAnswerEdit (e) {
const isAnswerValid = e.length < ANSWER_MAX_LENGTH && /^[a-z,.' ]*$/i.test (e);
if (isAnswerValid){
let newIntent = this.state.intent;
newIntent.answer = e;
this.setState ({intent: newIntent});
}
}
handleAnswerSourceEdit (e) {
const isAnswerSourceValid = e.length < ANSWER_MAX_LENGTH;
if (isAnswerSourceValid){
let newIntent = this.state.intent;
newIntent.externalAnswerSource = e;
this.setState ({intent: newIntent});
}
}
handleIntentNameEdit (e) {
const isIntentNameValid = e.length < INTENT_NAME_MAX_LENGTH && /^[a-z]*$/i.test (e);
if (isIntentNameValid){
let newIntent = this.state.intent;
newIntent.intentName = e;
this.setState ({intent: newIntent});
}
}
handleExternalSourceSave (answerType) {
let newIntent = this.state.intent;
newIntent.answerType = answerType;
this.setState ({intent: newIntent});
}
}
export default IntentDetails;

View File

@@ -0,0 +1,35 @@
import React, { Component } from 'react';
import {Button} from 'react-md';
import '../css/components/IntentItem.css'
import {INTENT_TITLE_MAX_LENGTH, INTENT_TITLE_TOOLTIP_DELAY} from '../config/constants'
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
disabled={this.props.waiting}
tooltipDelay={INTENT_TITLE_TOOLTIP_DELAY}
tooltipLabel={this.state.intent.intentName.length>INTENT_TITLE_MAX_LENGTH ? this.state.intent.questions[0] : ''}>
{buttonTitle}
</Button>
<br></br>
</div>
);
}
}
export default IntentItem;

View File

@@ -0,0 +1,54 @@
import React, { Component } from 'react';
import {Button} from 'react-md';
import IntentItem from './IntentItem';
import '../css/components/IntentList.css';
import {
LAUNCH_REQUEST_SELECTED_INDEX,
CONTACT_SELECTED_INDEX} from '../config/constants'
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===LAUNCH_REQUEST_SELECTED_INDEX ? "LaunchRequestButton-selected" : "LaunchRequestButton"} flat primary
onClick={this.props.onLaunchRequestClick}
disabled={this.props.waiting} >Launch request</Button>
<Button className={this.props.selectedIndex===CONTACT_SELECTED_INDEX ? "ContactButton-selected" : "ContactButton"} flat primary
onClick={this.props.onContactClick}
disabled={this.props.waiting} >Contact</Button>
<div className="IntentList-title">
<h3>Questions</h3>
</div>
{
this.state.intents.map((intent,index)=>{
return <IntentItem
key={intent.intentName} intent={intent} index={index}
selectedIndex={this.props.selectedIndex}
onClick={this.state.onIntentClick}
waiting={this.props.waiting}>
</IntentItem>
})
}
<br></br>
<Button className="AddIntent" flat primary swapTheming
onClick={this.props.onAddIntentClick}
disabled={this.props.waiting}>Add question</Button>
</div>
);
}
}
export default IntentList;

View File

@@ -0,0 +1,69 @@
import React, { Component } from 'react';
import {Button, TextField} from 'react-md';
import '../css/Common.css';
import '../css/components/LaunchRequest.css';
import { INVOCATION_NAME_MAX_LENGTH, INVOCATION_ANSWER_MAX_LENGTH } from '../config/constants';
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="RightPanelBox">
<h5 className="PanelSubTitle"> 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 InvocationInputBoxes"
maxLength={INVOCATION_NAME_MAX_LENGTH}
onChange={this.handleNameEdit}
value={this.state.invocationName}/>
<br></br>
<h5 className="PanelSubTitle" >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 InvocationInputBoxes"
maxLength={INVOCATION_ANSWER_MAX_LENGTH}
onChange={this.handleAnswerEdit}
value={this.state.invocationAnswer}/>
<br></br>
<br></br>
<Button className="SaveButton" flat primary swapTheming
onClick={()=>{this.props.onSaveClick(this.state.invocationName, this.state.invocationAnswer)}}
disabled={this.props.waiting}>Save</Button>
</div>
);
}
handleNameEdit(e){
const isInvocationNameValid = e.length < INVOCATION_NAME_MAX_LENGTH && (/^[a-z,.' ]*$/.test(e));
if (isInvocationNameValid) {
this.setState({invocationName: e});
}
}
handleAnswerEdit(e){
const isInvocationAnswerValid = e.length < INVOCATION_ANSWER_MAX_LENGTH && (/^[a-z,.' ]*$/i.test(e));
if (isInvocationAnswerValid){
this.setState({invocationAnswer: e});
}
}
}
export default LaunchRequest;

View File

@@ -0,0 +1,61 @@
import React from 'react';
import {Button} from 'react-md';
import {shallow, mount} from 'enzyme';
import AnswerSource from '../AnswerSource';
import {ANSWER_TYPE} from '../../config/constants';
it ('renders without crashing', () => {
shallow (<AnswerSource />);
});
describe ('functional tests', () => {
let wrapper;
beforeEach (() => {
const onSaveAnswerTypeFunction = jest.fn ();
wrapper = mount (
<AnswerSource onSaveAnswerType={onSaveAnswerTypeFunction} />
);
wrapper.setState ({
isModalOpen: false,
answerType: 0,
});
});
it ('snapshot', ()=>{
expect(wrapper).toMatchSnapshot();
});
it ('renders only a button', () => {
expect (wrapper.first ().text ()).toEqual ('Answer type');
expect (wrapper.find ('AnswerSourceForm').exists ()).toEqual (false);
});
it ('answer type button click opens modal form', () => {
expect (wrapper.find ('AnswerSourceFormA').exists ()).toEqual (false);
expect (wrapper.state ().isModalOpen).toEqual (false);
const AnswerTypeButton = wrapper.find ('button').first ();
AnswerTypeButton.simulate ('click');
expect (wrapper.state ().isModalOpen).toEqual (true);
expect (wrapper.find ('AnswerSourceForm').exists ()).toEqual (true);
expect (wrapper.find ('button').length).toBe (3);
expect (wrapper.find ('button').first ().text ()).toEqual ('Answer type');
});
it ('save button changes answerType value in state and closes the form ', () => {
const AnswerTypeButton = wrapper.find ('button').first ();
AnswerTypeButton.simulate ('click');
const saveButton = wrapper.find ('button').at (2);
const optionControl = wrapper.find ('SelectionControlGroup');
expect (saveButton.text ()).toEqual ('Save');
expect (optionControl.exists ()).toEqual (true);
optionControl.simulate ('change', {
target: {value: String (ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS)},
});
saveButton.simulate ('click');
expect (wrapper.state ().answerType).toBe (
ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS
);
expect (wrapper.state ().isModalOpen).toEqual (false);
expect (wrapper.find ('button').length).toBe (1);
});
});

View File

@@ -0,0 +1,318 @@
import React from 'react';
import {shallow, mount} from 'enzyme';
import IntentDetails from '../IntentDetails';
import {
QUESTION_MAX_LENGTH,
ANSWER_MAX_LENGTH,
INTENT_NAME_MAX_LENGTH,
INTENT_EXPLANATION_MAX_LENGTH,
ANSWER_TYPE,
} from '../../config/constants';
it ('renders without crashing', () => {
const dummyIntent = {
questions: ['q1', 'q2'],
};
shallow (<IntentDetails selectedIntent={dummyIntent} />);
});
describe('complete testing', () => {
let wrapper;
let newIntent;
beforeEach(()=>{
newIntent = {
intentName: '',
intentExplanation: '',
questions: [''],
answer: '',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
};
wrapper = mount(<IntentDetails selectedIntent={newIntent} />);
expect(wrapper.state('intent')).toEqual(newIntent);
});
it ('snapshot', () =>{
expect(wrapper).toMatchSnapshot();
});
it ('renders correctly for new intent input when empty intent is sent', () => {
expect(wrapper.find('TextField').length).toBe(4);
expect(wrapper.find('Button').length).toBe(4);
expect(wrapper.find('TextField').at(0).props().id).toEqual('intent explanation');
expect(wrapper.find('TextField').at(1).props().id).toEqual('intent name');
expect(wrapper.find('TextField').at(2).props().id).toEqual('intent question');
expect(wrapper.find('TextField').at(3).props().id).toEqual('intent answer');
expect(wrapper.find('Button').at(0).props().children).toEqual('add');
expect(wrapper.find('Button').at(1).props().children).toEqual('Answer type');
expect(wrapper.find('Button').at(2).props().children).toEqual('Save');
expect(wrapper.find('Button').at(3).props().children).toEqual('Delete');
});
it ('receives correct props for non empty intent with predefined answer', () => {
newIntent = {
intentName: 'Dummy intent',
intentExplanation: 'Dummy explanation',
questions: ['Dummy question'],
answer: 'dummy answer',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
};
wrapper = mount(<IntentDetails selectedIntent={newIntent} />);
expect(wrapper.state('intent')).toEqual(newIntent);
});
it ('receives correct props for non empty intent with external source for answer', () => {
newIntent = {
intentName: 'Dummy intent',
intentExplanation: 'Dummy explanation',
questions: ['Dummy question'],
answer: '',
answerType: ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS,
externalAnswerSource: 'http://sarajevotimes.com',
};
wrapper = mount(<IntentDetails selectedIntent={newIntent} />);
expect(wrapper.state('intent')).toEqual(newIntent);
});
it ('adds text field when add button is clicked', () => {
const addButton = wrapper.find('button').at(0);
addButton.simulate('click');
expect(wrapper.find('TextField').length).toBe(5);
addButton.simulate('click');
expect(wrapper.find('TextField').length).toBe(6);
expect(wrapper.find('TextField').at(0).props().id).toEqual('intent explanation');
expect(wrapper.find('TextField').at(1).props().id).toEqual('intent name');
expect(wrapper.find('TextField').at(2).props().id).toEqual('intent question');
expect(wrapper.find('TextField').at(3).props().id).toEqual('intent question');
expect(wrapper.find('TextField').at(4).props().id).toEqual('intent question');
expect(wrapper.find('TextField').at(5).props().id).toEqual('intent answer');
expect(wrapper.state('intent').questions.length).toBe(3);
});
it ('removes correct text field when delete button on text field is clicked', () => {
const addButton = wrapper.find('button').at(0);
addButton.simulate('click');
addButton.simulate('click');
let firstQuestionTextField = wrapper.find('TextField').at(2);
let secondQuestionTextField = wrapper.find('TextField').at(3);
let thirdQuestionTextField = wrapper.find('TextField').at(4);
firstQuestionTextField.instance().props.onChange('first question');
secondQuestionTextField.instance().props.onChange('second question');
thirdQuestionTextField.instance().props.onChange('third question');
expect(firstQuestionTextField.instance().value).toEqual('first question');
expect(secondQuestionTextField.instance().value).toEqual('second question');
expect(thirdQuestionTextField.instance().value).toEqual('third question');
expect(wrapper.state('intent').questions.length).toBe(3);
const rightIcon = secondQuestionTextField.props().rightIcon;
rightIcon.props.onClick(secondQuestionTextField.props().key);
expect(wrapper.state('intent').questions.length).toBe(2);
expect(secondQuestionTextField.instance().value).toEqual('third question');
expect(thirdQuestionTextField.instance()._field._field).toBeNull();
});
it ('does not remove text field when it is only one left', () => {
let firstQuestionTextField = wrapper.find('TextField').at(2);
firstQuestionTextField.instance().props.onChange('first question');
expect(firstQuestionTextField.props().id).toEqual('intent question');
expect(wrapper.state('intent').questions.length).toBe(1);
const rightIcon = firstQuestionTextField.props().rightIcon;
rightIcon.props.onClick(firstQuestionTextField.props().key);
expect(wrapper.state('intent').questions.length).toBe(1);
expect(firstQuestionTextField.instance().value).toEqual('first question');
});
it ('accepts text without special characters for intent explanation', () => {
let explanationTextField = wrapper.find('TextField').at(0);
let validExplanationText = 'to get latest news, say ';
explanationTextField.instance().props.onChange(validExplanationText);
expect(wrapper.state('intent').intentExplanation).toEqual(validExplanationText);
expect(explanationTextField.instance().value).toEqual(validExplanationText);
});
it ('does not accept text with special characters for intent explanation', () => {
let explanationTextField = wrapper.find('TextField').at(0);
let invalidExplanationText = '554to get latest news, say #$ ';
explanationTextField.instance().props.onChange(invalidExplanationText);
expect(wrapper.state('intent').intentExplanation).toEqual('');
expect(explanationTextField.instance().value).toEqual('');
});
it ('does not accept too long text for intent explanation', () => {
let explanationTextField = wrapper.find('TextField').at(0);
let invalidExplanationText = new Array(INTENT_EXPLANATION_MAX_LENGTH + 10).join('A');
explanationTextField.instance().props.onChange(invalidExplanationText);
expect(wrapper.state('intent').intentExplanation).toEqual('');
expect(explanationTextField.instance().value).toEqual('');
});
it ('accepts text without special characters for intent name', () => {
let intentNameTextField = wrapper.find('TextField').at(1);
let validIntentNameText = 'intentName';
intentNameTextField.instance().props.onChange(validIntentNameText);
expect(wrapper.state('intent').intentName).toEqual(validIntentNameText);
expect(intentNameTextField.instance().value).toEqual(validIntentNameText);
});
it ('does not accept text with speces characters for intent name', () => {
let intentNameTextField = wrapper.find('TextField').at(1);
let invalidIntentNameText = 'intentName with space';
intentNameTextField.instance().props.onChange(invalidIntentNameText);
expect(wrapper.state('intent').intentName).toEqual('');
expect(intentNameTextField.instance().value).toEqual('');
});
it ('does not accept text with special characters for intent name', () => {
let intentNameTextField = wrapper.find('TextField').at(1);
let invalidIntentNameText = 'intentName23!';
intentNameTextField.instance().props.onChange(invalidIntentNameText);
expect(wrapper.state('intent').intentName).toEqual('');
expect(intentNameTextField.instance().value).toEqual('');
});
it ('does not accept too long text for intent name', () => {
let intentNameTextField = wrapper.find('TextField').at(1);
let invalidIntentNameText = new Array(INTENT_NAME_MAX_LENGTH + 10).join('A');
intentNameTextField.instance().props.onChange(invalidIntentNameText);
expect(wrapper.state('intent').intentName).toEqual('');
expect(intentNameTextField.instance().value).toEqual('');
});
it ('accepts text without special characters for question text', () => {
let questionTextField = wrapper.find('TextField').at(2);
let validQuestionText = 'read me latest news'
questionTextField.instance().props.onChange(validQuestionText);
expect(wrapper.state('intent').questions).toEqual([validQuestionText]);
expect(questionTextField.instance().value).toEqual(validQuestionText);
});
it ('does not accept text with special characters for question text', () => {
let questionTextField = wrapper.find('TextField').at(2);
let invalidQuestionText = 'read m3 1at35t news #'
questionTextField.instance().props.onChange(invalidQuestionText);
expect(wrapper.state('intent').questions).toEqual(['']);
expect(questionTextField.instance().value).toEqual('');
});
it ('does not accept too long text for question text', () => {
let questionTextField = wrapper.find('TextField').at(2);
let invalidQuestionText = new Array(QUESTION_MAX_LENGTH + 10).join('A');
questionTextField.instance().props.onChange(invalidQuestionText);
expect(wrapper.state('intent').questions).toEqual(['']);
expect(questionTextField.instance().value).toEqual('');
});
it ('accepts text without special characters for answer text', () => {
let answerTextField = wrapper.find('TextField').at(3);
let validAnswerText = 'this is valid answer.'
answerTextField.instance().props.onChange(validAnswerText);
expect(wrapper.state('intent').answer).toEqual(validAnswerText);
expect(answerTextField.instance().value).toEqual(validAnswerText);
});
it ('does not accept text with special characters for answer text', () => {
let answerTextField = wrapper.find('TextField').at(3);
let invalidAnswerText = 'this is invalid answer.0123'
answerTextField.instance().props.onChange(invalidAnswerText);
expect(wrapper.state('intent').answer).toEqual('');
expect(answerTextField.instance().value).toEqual('');
});
it ('does not accept too long text for answer text', () => {
let answerTextField = wrapper.find('TextField').at(3);
let invalidAnswerText = new Array(ANSWER_MAX_LENGTH + 10).join('A');
answerTextField.instance().props.onChange(invalidAnswerText);
expect(wrapper.state('intent').answer).toEqual('');
expect(answerTextField.instance().value).toEqual('');
});
it ('accepts text for external source as answer', () => {
newIntent = {
intentName: '',
intentExplanation: '',
questions: [''],
answer: '',
answerType: ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS,
externalAnswerSource: '',
};
wrapper = mount(<IntentDetails selectedIntent={newIntent} />);
let answerTextField = wrapper.find('TextField').at(3);
let validAnswerText = 'http://sarajevotimes.com'
answerTextField.instance().props.onChange(validAnswerText);
expect(wrapper.state('intent').externalAnswerSource).toEqual(validAnswerText);
expect(answerTextField.instance().value).toEqual(validAnswerText);
});
it ('does not accept too long text for external source as answer', () => {
newIntent = {
intentName: '',
intentExplanation: '',
questions: [''],
answer: '',
answerType: ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS,
externalAnswerSource: '',
};
wrapper = mount(<IntentDetails selectedIntent={newIntent} />);
let answerTextField = wrapper.find('TextField').at(3);
let invalidAnswerText = new Array(ANSWER_MAX_LENGTH + 10).join('A');
answerTextField.instance().props.onChange(invalidAnswerText);
expect(wrapper.state('intent').answer).toEqual('');
expect(answerTextField.instance().value).toEqual('');
});
it ('calls function with correct data on save button click', () => {
newIntent = {
intentName: 'Dummy intent',
intentExplanation: 'Dummy explanation',
questions: ['Dummy question'],
answer: 'Dummy answer',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
};
const onSaveFunction = jest.fn();
wrapper = mount(<IntentDetails selectedIntent={newIntent} onSaveIntentClick={onSaveFunction} />);
wrapper.find('Button').at(2).simulate('click');
expect(onSaveFunction).toBeCalledWith(newIntent);
});
it ('calls function with correct data on delete button click', () => {
newIntent = {
intentName: 'Dummy intent',
intentExplanation: 'Dummy explanation',
questions: ['Dummy question'],
answer: 'Dummy answer',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
};
const onSaveFunction = jest.fn();
wrapper = mount(<IntentDetails selectedIntent={newIntent} onDeleteIntentClick={onSaveFunction} />);
wrapper.find('Button').at(3).simulate('click');
expect(onSaveFunction).toBeCalledWith(newIntent);
});
});

View File

@@ -0,0 +1,93 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`functional tests snapshot 1`] = `
<AnswerSource
onSaveAnswerType={[Function]}
>
<div>
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
>
<withTooltip(Button)
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
>
<Button
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-text--theme-primary md-ink--primary md-inline-block"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Answer type
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
</div>
</AnswerSource>
`;

View File

@@ -0,0 +1,898 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`complete testing snapshot 1`] = `
<IntentDetails
selectedIntent={
Object {
"answer": "",
"answerType": 0,
"externalAnswerSource": "",
"intentExplanation": "",
"intentName": "",
"questions": Array [
"",
],
}
}
>
<div
className="RightPanelBox"
>
<div
className="QuestionBox"
>
<h5
className="PanelSubTitle"
>
In introduction, Alexa will help users to ask her the right questions about your business. For Example, she will say : "To ask us about our services, say : What do you do ? ". What do you do ? is defined in question field. Alexa will use first variation of question in intro.
</h5>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent explanation"
leftIconStateful={true}
lineDirection="center"
maxLength={70}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="To ask us about our services, say "
rightIconStateful={true}
type="text"
value=""
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={false}
htmlFor="intent explanation"
iconOffset={false}
key="label"
/>
<InputField
className=""
fullWidth={true}
id="intent explanation"
inlineIndicator={false}
key="field"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="To ask us about our services, say "
type="text"
value=""
>
<input
className="md-text-field md-text-field--margin md-full-width md-text"
id="intent explanation"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="To ask us about our services, say "
type="text"
value=""
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={0}
error={false}
key="message"
leftIcon={false}
maxLength={70}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
0 / 70
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
<br />
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent name"
label="Question name"
leftIconStateful={true}
lineDirection="center"
maxLength={30}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
rightIconStateful={true}
type="text"
value=""
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={false}
htmlFor="intent name"
iconOffset={false}
key="label"
label="Question name"
>
<label
className="md-floating-label md-floating-label--inactive md-floating-label--inactive-sized md-text--secondary"
htmlFor="intent name"
>
Question name
</label>
</FloatingLabel>
<InputField
className=""
fullWidth={true}
id="intent name"
inlineIndicator={false}
key="field"
label="Question name"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder={null}
type="text"
value=""
>
<input
className="md-text-field md-text-field--floating-margin md-full-width md-text"
id="intent name"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder={null}
type="text"
value=""
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={0}
error={false}
key="message"
leftIcon={false}
maxLength={30}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
0 / 30
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
<h5
className="QuestionTitle"
>
Question variants
</h5>
<div
className="QuestionBox"
key="0"
>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent question"
leftIconStateful={true}
lineDirection="center"
maxLength={150}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="Question"
rightIcon={
<SVGIcon
focusable="false"
onClick={[Function]}
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
fill="#000000"
/>
</SVGIcon>
}
rightIconStateful={true}
type="text"
value=""
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={false}
htmlFor="intent question"
iconOffset={false}
key="label"
/>
<div
className="md-text-field-icon-container"
key="icon-divider"
>
<div
className="md-text-field-divider-container md-text-field-divider-container--grow"
key="divider-container"
>
<InputField
className=""
fullWidth={true}
id="intent question"
inlineIndicator={false}
key="field"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Question"
type="text"
value=""
>
<input
className="md-text-field md-text-field--margin md-full-width md-text"
id="intent question"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Question"
type="text"
value=""
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
</div>
<SVGIcon
className="md-text-field-icon md-text-field-icon--positioned"
error={false}
focusable="false"
key="icon-right"
onClick={[Function]}
primary={false}
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<svg
aria-labelledby={null}
className="md-icon md-text-field-icon md-text-field-icon--positioned"
focusable="false"
onClick={[Function]}
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
fill="#000000"
/>
</svg>
</SVGIcon>
</div>
<TextFieldMessage
active={false}
currentLength={0}
error={false}
key="message"
leftIcon={false}
maxLength={150}
rightIcon={true}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-text-field-message-container--right-icon-offset md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
0 / 150
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
<withInk(withTooltip(Button))
className="AddQuestionVariantButton"
icon={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
>
<withTooltip(Button)
className="AddQuestionVariantButton"
icon={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
>
<Button
className="AddQuestionVariantButton"
fixedPosition="br"
icon={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
type="button"
>
<button
className="md-btn md-btn--icon md-pointer--hover md-text--theme-primary md-ink--primary md-inline-block AddQuestionVariantButton"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
<FontIcon
iconClassName="material-icons"
inherit={true}
>
<i
className="md-icon material-icons md-text--inherit"
>
add
</i>
</FontIcon>
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
<AnswerSource
answerType={0}
className="AnswerTypeButton"
onSaveAnswerType={[Function]}
>
<div>
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
>
<withTooltip(Button)
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
>
<Button
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-text--theme-primary md-ink--primary md-inline-block"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Answer type
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
</div>
</AnswerSource>
<AnswerTextBox
answer=""
answerType={0}
externalAnswerSource=""
handleAnswerEdit={[Function]}
handleAnswerSourceEdit={[Function]}
>
<div
className="QuestionBox"
>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent answer"
label="Answer"
leftIconStateful={true}
lineDirection="center"
maxLength={150}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="Answer"
rightIconStateful={true}
type="text"
value=""
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={false}
htmlFor="intent answer"
iconOffset={false}
key="label"
label="Answer"
>
<label
className="md-floating-label md-floating-label--inactive md-floating-label--inactive-sized md-text--secondary"
htmlFor="intent answer"
>
Answer
</label>
</FloatingLabel>
<InputField
className=""
fullWidth={true}
id="intent answer"
inlineIndicator={false}
key="field"
label="Answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder={null}
type="text"
value=""
>
<input
className="md-text-field md-text-field--floating-margin md-full-width md-text"
id="intent answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder={null}
type="text"
value=""
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={0}
error={false}
key="message"
leftIcon={false}
maxLength={150}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
0 / 150
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
</AnswerTextBox>
<withInk(withTooltip(Button))
className="IntentDetailsButton-firstInRow"
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
swapTheming={true}
>
<withTooltip(Button)
className="IntentDetailsButton-firstInRow"
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
swapTheming={true}
>
<Button
className="IntentDetailsButton-firstInRow"
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
swapTheming={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-background--primary md-background--primary-hover md-inline-block IntentDetailsButton-firstInRow"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Save
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
<withInk(withTooltip(Button))
className="IntentDetailsButton"
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
>
<withTooltip(Button)
className="IntentDetailsButton"
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
>
<Button
className="IntentDetailsButton"
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-text--theme-primary md-ink--primary md-inline-block IntentDetailsButton"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Delete
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
</div>
</IntentDetails>
`;

View File

@@ -0,0 +1,57 @@
import React, {Component} from 'react';
import {Button, SelectionControlGroup} from 'react-md';
import Modal from './Modal';
class AnswerSourceForm extends Component{
render(){
return(
<Modal
title="Answer type"
actions={[
<Button
flat
swapTheming
onClick={this.props.onClose.bind (this)}
key="cancel"
>
Cancel
</Button>,
<Button
flat
primary
swapTheming
key="save"
onClick={this.props.onSave.bind(this)}
>
Save
</Button>,
]}
>
<SelectionControlGroup
id="answer-source"
name="answer-source"
type="radio"
label="Import answer from:"
onChange={this.props.onSourceChange.bind(this)}
controls={[
{
label: 'Predefined answer',
value: '0'
},
{
label: 'WordPress titles',
value: '1',
},
{
label: 'WordPress latest news',
value: '2',
},
]}
defaultValue={String(this.props.answerType)}
/>
</Modal>);
}
}
export default AnswerSourceForm;

View File

@@ -0,0 +1,44 @@
import React, {Component} from 'react';
import {TextField} from 'react-md';
import '../../css/components/IntentDetails.css';
import '../../css/Common.css';
import {
ANSWER_MAX_LENGTH,
ANSWER_TYPE,
} from '../../config/constants';
class AnswerTextBox extends Component {
render () {
//theese are defaults for ANSWER_TYPE.PREDEFINED
let labelText="Answer";
let valueText=this.props.answer;
let onChangeValue=this.props.handleAnswerEdit;
switch(this.props.answerType){
case ANSWER_TYPE.EXTERNAL_SOURCE_WP_TITLES:
case ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS:
labelText="Answer source";
valueText=this.props.externalAnswerSource;
onChangeValue=this.props.handleAnswerSourceEdit
break;
}
return(
<div className="QuestionBox">
<TextField
id="intent answer"
lineDirection="center"
label={labelText}
placeholder={labelText}
maxLength={ANSWER_MAX_LENGTH}
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
onChange={onChangeValue}
value={valueText}
/>
</div>
);
}
}
export default AnswerTextBox;

View File

@@ -0,0 +1,23 @@
import React, { Component } from 'react';
import '../../css/components/Modal.css';
class Modal extends Component {
render() {
const { title, children, actions } = this.props;
return (
<div className="modal">
<div className="modal-content">
<h2 className="header">
{title}
</h2>
{children}
<div className="actions">
{actions}
</div>
</div>
</div>
);
}
}
export default Modal;

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import AnswerSourceForm from '../AnswerSourceForm';
import {ANSWER_TYPE} from '../../../config/constants'
it('renders without crashing', () => {
shallow(<AnswerSourceForm onClose={()=>{}} onSave={()=>{}} onSourceChange={()=>{}} />);
});
it ('snapshot',()=>{
const wrapper = mount(<AnswerSourceForm onClose={()=>{}} onSave={()=>{}} onSourceChange={()=>{}} />);
expect(wrapper).toMatchSnapshot();
})
it('calls onClose when cancel is pressed', () => {
const onClose = jest.fn();
const wrapper = mount(<AnswerSourceForm onClose={onClose} onSave={()=>{}} onSourceChange={()=>{}} />);
const cancelButton = wrapper.find('button').at(0);
expect(cancelButton.text()).toEqual('Cancel');
cancelButton.simulate('click');
expect(onClose).toBeCalled();
});
it('calls onSave when save is pressed', () => {
const onSave = jest.fn();
const wrapper = mount(<AnswerSourceForm onClose={()=>{}} onSave={onSave} onSourceChange={()=>{}} />);
const saveButton = wrapper.find('button').at(1);
expect(saveButton.text()).toEqual('Save');
saveButton.simulate('click');
expect(onSave).toBeCalled();
});
it('sets PREDEFINED value when Predefined answer is selected', () => {
let selectedValue = null;
const wrapper = mount(<AnswerSourceForm
onClose={()=>{}}
onSave={()=>{}}
onSourceChange={(value)=>{selectedValue=value}} />);
const optionControl = wrapper.find('SelectionControlGroup');
expect(optionControl.exists()).toEqual(true);
optionControl.simulate('change',{target:{value:String(ANSWER_TYPE.PREDEFINED)}});
expect(selectedValue).toBe(String(ANSWER_TYPE.PREDEFINED));
});
it('sets EXTERNAL_SOURCE_WP_TITLES value when WordPress titles is selected', () => {
let selectedValue = null;
const wrapper = mount(<AnswerSourceForm
onClose={()=>{}}
onSave={()=>{}}
onSourceChange={(value)=>{selectedValue=value}} />);
const optionControl = wrapper.find('SelectionControlGroup');
expect(optionControl.exists()).toEqual(true);
optionControl.simulate('change',{target:{value:String(ANSWER_TYPE.EXTERNAL_SOURCE_WP_TITLES)}});
expect(selectedValue).toBe(String(ANSWER_TYPE.EXTERNAL_SOURCE_WP_TITLES));
});
it('sets EXTERNAL_SOURCE_WP_NEWS value when WordPress latest news is selected', () => {
let selectedValue = null;
const wrapper = mount(<AnswerSourceForm
onClose={()=>{}}
onSave={()=>{}}
onSourceChange={(value)=>{selectedValue=value}} />);
const optionControl = wrapper.find('SelectionControlGroup');
expect(optionControl.exists()).toEqual(true);
optionControl.simulate('change',{target:{value:String(ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS)}});
expect(selectedValue).toBe(String(ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS));
});

View File

@@ -0,0 +1,93 @@
import React from 'react';
import {shallow, mount} from 'enzyme';
import AnswerTextBox from '../AnswerTextBox';
import {ANSWER_TYPE} from '../../../config/constants';
it ('renders without crashing', () => {
shallow (<AnswerTextBox />);
});
describe ('predefined answer selected', () => {
let wrapper;
let textField;
beforeEach (() => {
const onChange = jest.fn();
wrapper = mount (
<AnswerTextBox
answerType={ANSWER_TYPE.PREDEFINED}
answer={'Dummy answer'}
handleAnswerEdit={onChange}
/>
);
textField = wrapper.find ('TextField').first ();
});
it ('snapshot', () =>{
expect(wrapper).toMatchSnapshot();
});
it ('renders text box for normal answer', () => {
expect (textField.props ().label).toEqual ('Answer');
});
it ('receives valid answer text', () => {
expect (textField.props ().value).toEqual ('Dummy answer');
});
});
describe ('WordPress titles selected', () => {
let wrapper;
let textField;
beforeEach (() => {
const onChange = jest.fn();
wrapper = mount (
<AnswerTextBox
answerType={ANSWER_TYPE.EXTERNAL_SOURCE_WP_TITLES}
externalAnswerSource={'Dummy answer'}
handleAnswerSourceEdit={onChange}
/>
);
textField = wrapper.find ('TextField').first ();
});
it ('snapshot', () =>{
expect(wrapper).toMatchSnapshot();
});
it ('renders text box for external source input', () => {
expect (textField.props ().label).toEqual ('Answer source');
});
it ('receives valid answer text', () => {
expect (textField.props ().value).toEqual ('Dummy answer');
});
});
describe ('WordPress latest news selected', () => {
let wrapper;
let textField;
beforeEach (() => {
const onChange = jest.fn();
wrapper = mount (
<AnswerTextBox
answerType={ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS}
externalAnswerSource={'Dummy answer'}
handleAnswerSourceEdit={onChange}
/>
);
textField = wrapper.find ('TextField').first ();
});
it ('snapshot', () =>{
expect(wrapper).toMatchSnapshot();
});
it ('renders text box for external source input', () => {
expect (textField.props ().label).toEqual ('Answer source');
});
it ('receives valid answer text', () => {
expect (textField.props ().value).toEqual ('Dummy answer');
});
});

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import Modal from '../Modal';
it('renders without crashing', () => {
shallow(<Modal />);
});
let actionButton;
let childButton;
let wrapper;
beforeEach(()=>{
actionButton = <button key={0}>Dummy action button</button>;
childButton = <button key={1}>Child button</button>;
wrapper = mount(<Modal title={'Dummy title'} actions={[actionButton]}>{childButton}</Modal>);
});
it ('snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
it('receives props as expected', () =>{
expect(wrapper.props().title).toEqual('Dummy title');
expect(wrapper.props().actions).toEqual([actionButton]);
expect(wrapper.props().children).toEqual(childButton);
});

View File

@@ -0,0 +1,675 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`snapshot 1`] = `
<AnswerSourceForm
onClose={[Function]}
onSave={[Function]}
onSourceChange={[Function]}
>
<Modal
actions={
Array [
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
swapTheming={true}
>
Cancel
</withInk(withTooltip(Button))>,
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
swapTheming={true}
>
Save
</withInk(withTooltip(Button))>,
]
}
title="Answer type"
>
<div
className="modal"
>
<div
className="modal-content"
>
<h2
className="header"
>
Answer type
</h2>
<SelectionControlGroup
component="fieldset"
controls={
Array [
Object {
"label": "Predefined answer",
"value": "0",
},
Object {
"label": "WordPress titles",
"value": "1",
},
Object {
"label": "WordPress latest news",
"value": "2",
},
]
}
defaultValue="undefined"
id="answer-source"
label="Import answer from:"
labelClassName="md-subheading-1"
labelComponent="legend"
name="answer-source"
onChange={[Function]}
type="radio"
>
<fieldset
className="md-selection-control-group"
onChange={[Function]}
onKeyDown={[Function]}
>
<legend
className="md-subheading-1"
>
Import answer from:
</legend>
<SelectionControl
checked={false}
checkedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box
</FontIcon>
}
checkedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_checked
</FontIcon>
}
className=""
id="answer-source0"
key="control0"
label="Predefined answer"
name="answer-source"
type="radio"
uncheckedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box_outline_blank
</FontIcon>
}
uncheckedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_unchecked
</FontIcon>
}
value="0"
>
<div
className="md-selection-control-container"
onKeyDown={[Function]}
>
<input
aria-hidden={true}
checked={false}
className="md-selection-control-input"
id="answer-source0"
name="answer-source"
onChange={[Function]}
type="radio"
value="0"
/>
<label
className="md-selection-control-label md-pointer--hover md-text"
htmlFor="answer-source0"
>
<withInk(AccessibleFakeButton)
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
role="radio"
>
<AccessibleFakeButton
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
component="div"
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
listenToEnter={true}
listenToSpace={true}
noFocusOutline={true}
role="radio"
tabIndex={0}
>
<div
aria-checked={false}
aria-pressed={false}
className="md-fake-btn md-pointer--hover md-fake-btn--no-outline md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="radio"
tabIndex={0}
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
<FontIcon
iconClassName="material-icons"
inherit={true}
key=".1"
>
<i
className="md-icon material-icons md-text--inherit"
>
radio_button_unchecked
</i>
</FontIcon>
</div>
</AccessibleFakeButton>
</withInk(AccessibleFakeButton)>
<span>
Predefined answer
</span>
</label>
</div>
</SelectionControl>
<SelectionControl
checked={false}
checkedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box
</FontIcon>
}
checkedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_checked
</FontIcon>
}
className=""
id="answer-source1"
key="control1"
label="WordPress titles"
name="answer-source"
tabIndex={-1}
type="radio"
uncheckedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box_outline_blank
</FontIcon>
}
uncheckedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_unchecked
</FontIcon>
}
value="1"
>
<div
className="md-selection-control-container"
onKeyDown={[Function]}
>
<input
aria-hidden={true}
checked={false}
className="md-selection-control-input"
id="answer-source1"
name="answer-source"
onChange={[Function]}
type="radio"
value="1"
/>
<label
className="md-selection-control-label md-pointer--hover md-text"
htmlFor="answer-source1"
>
<withInk(AccessibleFakeButton)
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
role="radio"
tabIndex={-1}
>
<AccessibleFakeButton
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
component="div"
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
listenToEnter={true}
listenToSpace={true}
noFocusOutline={true}
role="radio"
tabIndex={-1}
>
<div
aria-checked={false}
aria-pressed={false}
className="md-fake-btn md-pointer--hover md-fake-btn--no-outline md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="radio"
tabIndex={-1}
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
<FontIcon
iconClassName="material-icons"
inherit={true}
key=".1"
>
<i
className="md-icon material-icons md-text--inherit"
>
radio_button_unchecked
</i>
</FontIcon>
</div>
</AccessibleFakeButton>
</withInk(AccessibleFakeButton)>
<span>
WordPress titles
</span>
</label>
</div>
</SelectionControl>
<SelectionControl
checked={false}
checkedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box
</FontIcon>
}
checkedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_checked
</FontIcon>
}
className=""
id="answer-source2"
key="control2"
label="WordPress latest news"
name="answer-source"
tabIndex={-1}
type="radio"
uncheckedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box_outline_blank
</FontIcon>
}
uncheckedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_unchecked
</FontIcon>
}
value="2"
>
<div
className="md-selection-control-container"
onKeyDown={[Function]}
>
<input
aria-hidden={true}
checked={false}
className="md-selection-control-input"
id="answer-source2"
name="answer-source"
onChange={[Function]}
type="radio"
value="2"
/>
<label
className="md-selection-control-label md-pointer--hover md-text"
htmlFor="answer-source2"
>
<withInk(AccessibleFakeButton)
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
role="radio"
tabIndex={-1}
>
<AccessibleFakeButton
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
component="div"
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
listenToEnter={true}
listenToSpace={true}
noFocusOutline={true}
role="radio"
tabIndex={-1}
>
<div
aria-checked={false}
aria-pressed={false}
className="md-fake-btn md-pointer--hover md-fake-btn--no-outline md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="radio"
tabIndex={-1}
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
<FontIcon
iconClassName="material-icons"
inherit={true}
key=".1"
>
<i
className="md-icon material-icons md-text--inherit"
>
radio_button_unchecked
</i>
</FontIcon>
</div>
</AccessibleFakeButton>
</withInk(AccessibleFakeButton)>
<span>
WordPress latest news
</span>
</label>
</div>
</SelectionControl>
</fieldset>
</SelectionControlGroup>
<div
className="actions"
>
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
key="cancel"
onClick={[Function]}
swapTheming={true}
>
<withTooltip(Button)
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
swapTheming={true}
>
<Button
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
swapTheming={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-text md-inline-block"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Cancel
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
key="save"
onClick={[Function]}
primary={true}
swapTheming={true}
>
<withTooltip(Button)
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
swapTheming={true}
>
<Button
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
swapTheming={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-background--primary md-background--primary-hover md-inline-block"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Save
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
</div>
</div>
</div>
</Modal>
</AnswerSourceForm>
`;

View File

@@ -0,0 +1,376 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WordPress latest news selected snapshot 1`] = `
<AnswerTextBox
answerType={2}
externalAnswerSource="Dummy answer"
handleAnswerSourceEdit={[Function]}
>
<div
className="QuestionBox"
>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent answer"
label="Answer source"
leftIconStateful={true}
lineDirection="center"
maxLength={150}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="Answer source"
rightIconStateful={true}
type="text"
value="Dummy answer"
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={true}
htmlFor="intent answer"
iconOffset={false}
key="label"
label="Answer source"
>
<label
className="md-floating-label md-floating-label--floating md-text--secondary"
htmlFor="intent answer"
>
Answer source
</label>
</FloatingLabel>
<InputField
className=""
fullWidth={true}
id="intent answer"
inlineIndicator={false}
key="field"
label="Answer source"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer source"
type="text"
value="Dummy answer"
>
<input
className="md-text-field md-text-field--floating-margin md-full-width md-text"
id="intent answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer source"
type="text"
value="Dummy answer"
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={12}
error={false}
key="message"
leftIcon={false}
maxLength={150}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
12 / 150
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
</AnswerTextBox>
`;
exports[`WordPress titles selected snapshot 1`] = `
<AnswerTextBox
answerType={1}
externalAnswerSource="Dummy answer"
handleAnswerSourceEdit={[Function]}
>
<div
className="QuestionBox"
>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent answer"
label="Answer source"
leftIconStateful={true}
lineDirection="center"
maxLength={150}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="Answer source"
rightIconStateful={true}
type="text"
value="Dummy answer"
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={true}
htmlFor="intent answer"
iconOffset={false}
key="label"
label="Answer source"
>
<label
className="md-floating-label md-floating-label--floating md-text--secondary"
htmlFor="intent answer"
>
Answer source
</label>
</FloatingLabel>
<InputField
className=""
fullWidth={true}
id="intent answer"
inlineIndicator={false}
key="field"
label="Answer source"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer source"
type="text"
value="Dummy answer"
>
<input
className="md-text-field md-text-field--floating-margin md-full-width md-text"
id="intent answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer source"
type="text"
value="Dummy answer"
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={12}
error={false}
key="message"
leftIcon={false}
maxLength={150}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
12 / 150
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
</AnswerTextBox>
`;
exports[`predefined answer selected snapshot 1`] = `
<AnswerTextBox
answer="Dummy answer"
answerType={0}
handleAnswerEdit={[Function]}
>
<div
className="QuestionBox"
>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent answer"
label="Answer"
leftIconStateful={true}
lineDirection="center"
maxLength={150}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="Answer"
rightIconStateful={true}
type="text"
value="Dummy answer"
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={true}
htmlFor="intent answer"
iconOffset={false}
key="label"
label="Answer"
>
<label
className="md-floating-label md-floating-label--floating md-text--secondary"
htmlFor="intent answer"
>
Answer
</label>
</FloatingLabel>
<InputField
className=""
fullWidth={true}
id="intent answer"
inlineIndicator={false}
key="field"
label="Answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer"
type="text"
value="Dummy answer"
>
<input
className="md-text-field md-text-field--floating-margin md-full-width md-text"
id="intent answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer"
type="text"
value="Dummy answer"
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={12}
error={false}
key="message"
leftIcon={false}
maxLength={150}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
12 / 150
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
</AnswerTextBox>
`;

View File

@@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`snapshot 1`] = `
<Modal
actions={
Array [
<button>
Dummy action button
</button>,
]
}
title="Dummy title"
>
<div
className="modal"
>
<div
className="modal-content"
>
<h2
className="header"
>
Dummy title
</h2>
<button
key="1"
>
Child button
</button>
<div
className="actions"
>
<button
key="0"
>
Dummy action button
</button>
</div>
</div>
</div>
</Modal>
`;

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

@@ -0,0 +1,4 @@
//export const BASE_URL = 'tellall.saburly.com'; //for server
export const BASE_URL = 'localhost:5000'; //for local

View File

@@ -0,0 +1,34 @@
export const INTENT_EXPLANATION_MAX_LENGTH = 70;
export const INTENT_NAME_MAX_LENGTH = 30;
export const INTENT_NAME_MIN_LENGTH = 2;
export const QUESTION_MAX_LENGTH = 150;
export const QUESTION_MIN_LENGTH = 2;
export const ANSWER_MAX_LENGTH = 150;
export const ANSWER_MIN_LENGTH = 2;
export const INTENT_TITLE_MAX_LENGTH = 20;
export const INTENT_TITLE_TOOLTIP_DELAY = 700;
export const INVOCATION_NAME_MAX_LENGTH = 50;
export const INVOCATION_NAME_MIN_LENGTH = 2;
export const INVOCATION_ANSWER_MAX_LENGTH = 100;
export const EMAIL_MAX_LENGTH = 100;
export const NEW_INTENT_SELECTED_INDEX = -1;
export const LAUNCH_REQUEST_SELECTED_INDEX = -2;
export const CONTACT_SELECTED_INDEX = -3;
export const ANSWER_TYPE = {
PREDEFINED: 0,
EXTERNAL_SOURCE_WP_TITLES : 1,
EXTERNAL_SOURCE_WP_NEWS : 2
}
export const RESULT_CODES = {
OK: 0,
ERROR: -1,
};

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;
}

21
web/src/css/Common.scss Normal file
View File

@@ -0,0 +1,21 @@
$minHeight : calc(100vh - 80px); //80px is height of the title div container
/* Common for right panel components */
.RightPanelBox{
float: left;
width: 70%;
min-height:$minHeight;
background-color: #f5f5f5;
}
.PanelSubTitle{
text-align: left;
margin-top: 30px;
margin-left: 20px;
}
.SaveButton{
float: right;
margin-right: 20px;
}

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;

View File

@@ -0,0 +1,4 @@
.ContactEmailInput{
width: 60%;
margin-left: 20px;
}

View File

@@ -0,0 +1,33 @@
.QuestionBox{
margin:25px;
}
.QuestionTitle{
margin-top:20px;
margin-left: 30px;
float: left;
}
.IntentDetailsInputBoxes{
width: 90%;
}
.IntentDetailsButton{
float: right;
margin-right: 25px;
}
.IntentDetailsButton-firstInRow{
float: right;
margin-right: 10%;
}
.AddQuestionVariantButton{
float: right;
margin-right: 10%;
}
.AnswerTypeButton{
float: right;
margin-right: 30px;
}

View File

@@ -0,0 +1,18 @@
.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;
}

View File

@@ -0,0 +1,52 @@
$minHeight : calc(100vh - 80px); //80px is height of the title div container
.IntentList{
width: 30%;
min-height:$minHeight;
float:left;
background-color: #eff0f0;
}
.IntentList-title{
font-size: 1.5em;
height: 70px;
padding: 20px;
text-align:left;
background-color: #eff0f0;
}
.AddIntent{
float: right;
margin: 12px;
}
.LaunchRequestButton{
text-align: left;
width: 100%;
height: 50px;
background-color: #d8d8d8
}
.LaunchRequestButton-selected{
text-align: left;
width: 100%;
height: 50px;
background-color: #f5f5f5;
}
.ContactButton{
text-align: left;
color: #009b8a;
width: 100%;
height: 50px;
background-color: #d8d8d8
}
.ContactButton-selected{
text-align: left;
color: #009b8a;
width: 100%;
height: 50px;
background-color: #f5f5f5;
}

View File

@@ -0,0 +1,11 @@
.ExplanationText{
float: left;
margin-top: 30px;
margin-left: 20px;
text-align: left;
}
.InvocationInputBoxes{
width: 60%;
margin-left: 20px;
}

View File

@@ -0,0 +1,32 @@
.modal {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(37, 37, 37, .7);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
.modal-content {
color: black;
padding: 20px;
padding-bottom: 0px;
display: flex;
flex-direction: column;
align-items: flex-start;
height: auto;
background-color: #FFF;
min-width: 500px;
.actions {
border-top: 1px solid #bebebe;
width: 100%;
display: flex;
justify-content: flex-end;
min-width: 500px;
margin: 0 -20px;
padding: 10px;
}
}
}

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

File diff suppressed because it is too large Load Diff

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

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

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

@@ -0,0 +1,174 @@
.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, 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,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAB8BJREFUWAnFWAtsU1UY/s+5XTcYYxgfvERQeQXxNeYLjVFxLVvb2xasKIgSVNQoREVI1GhmfC6ioijiNDo1vBxb19uVtRWUzAQ1+EowOkSQzTBAUJio27r2Hr9TLJTaa7vK4yTtvec///+f7/znf5xzGf2PZnVMKRHUczEJNpgYDSEdPzTB6GdG1EbE2sxk+qqxsW5rrtNAT+/aZLtrkiDdLYhUIcSwQ9KsA7DaAbKdEWOCQBckxwrkOGP0Lf7rTAqrW+vzbT4kk91/1gAB7BqdYlVC0KUAsQuANOKKjwYUNYfff//PdNNZ3O4zqEe/FguZykhUYFGFQKspnBYGNW1LOplUWkaANtvUc3pY5FUAKwewb4jzR0KaN8ikoXrRZs2aVbBr3/6bddKfhHUHAugys+j3eCCwYv9/qflPgFab83ps52ookxZ6OOT3regtsNTJHY45fSO05yGh6wsFsZ1cIVtI035M5Uv0DQFabY77BWOLsNrmQrPi8Xq9vyaEjsXT4pg6VuiRABZfzAVzhwK+T9Lp5emIFru6QCd6CXv4+sRLSizHGpycM+yvayng/S6Do7QIJtZZVXVyOiz/sqDV4XAKweoxsDjUqM1PJ3QsaeVz5+bHtrc2IjWVmky8tKmhYVuy/qMsWOZyXSR0Wo4IDVxRWrIgmfF4vTctWdINF7oJljwQ7dG9lpkzC5PnOgywsrKSU1R/Gz6xo7hPwXT0scsnpkkXEnncjTw6kvZ3vJI8q5Lo5BUV3YaAuFthyjStof6HBP1EPbe3tOweNWpMF0AuGHveuNqtLS375NxxC8rQB7inkOd8wcaGDScKVOo8/fvmLwWOPZFIrDIxFgcYEbtnA9wgk1lZmBgwetrtnqGTbapqNG5Et06ZMhhuYzIal/Ta2tpOlMVnEAOeCqfzfEmLA0SV8KB+bljr9Wbc2ijrujpGwmdxOB+SCrJpckGiu+enT7/85uZM/P375FcjDn6LxsRMycsrPJ5B2PerOLE1mYTleNDvX8k4W4xK8HyZ3XlvJpkym+qJEa1B1VjHRwz7IBM/rBjBNodhxXLJy6N/dbvlSz4nr3xm08J+7QHkyTdI6EssDsftRjJWh2smtmwlyrZ29tBBbplSjHiT6ZyxIHZ1vHQnVBlRArTfaZq2J5kp0zuS+D2w5Hs4/FWj8sxI5bfa1TuF0GtAX4W0Na26uronlceon89FSI5FRPf1HJY4C2e1HUbMRnR5aCguyIf1RC143oW1piZ44Z/zdCFgYXpnYmnJrdg27HL2LW4sxg7A9YYhqthwEmJ99uJHOOXEiMxbNm76qkAX+kps9xSUyXHwzyps02tBv29urqcfGG4fzgKnIYrFMHTajkzbuzcAjBb3zb8ROtajTHqx2Cq8L4IL3JcruEMIxF4cck/niK4IjlV5vYN1NLeMPATDd6DKPBclhfmP5sipdxBSRdKCe/E7PScVEMJxnllszlfgcw/CYk8g4X8OSwbKHY7Lc9Up5aB2MNxvN2eC7UUnJ4DYXm51ON/AqXsuVvpAuFGrVAYUVUD991HBmuStL1eQ2N7hkG1DfqY92J4ze6vI4/EoCI53YcE7EBD3hAL+xVJH0/Llv5tFkRUTtOoiGrbY3ONz0F2MAOnPGG8FQLYRCi7DhP2yVTRnzpy8A391r8TipqNYzkZALEuWlRchpU9BGfbpF8Fi6yar6pjk8UzvBzt7SuM8grbwPBMPwArm37u6JmUSlOPyBLyjfVcdttGNPDfjQ7+/Jp1cU23tXp6fNwkRfTCmi/XydpiOLx0tRvoNWPzOoN+7iQe83u/h2Dvgh7Z0zKk0/afWF+C8VsYVTzigrUodT+6H6ut3IaKvw0KiEYp8pKpqUfJ4unfp16C7meD1Mk3JDprwovbdaLNNP+VQ3/hfKGwFJ+WasL+hwZjryEjY5/vZTObrYJFmznHJzNA+2/S1dI2BsLysUBBDw8qGdOr0Ixz75XCj/2FJOxlNpiyrQ/0CuZmF/b4Jhy2I2ie/qywFqHkAO/BkgJNzWu3OW7GTJZzT/EQV+meL5Veewudg0FhnjJacDIAul2sATlZPw3gavjR8nMBwGCDOofuA+m74o0de3BMMJ+KJwDD9GY2twdGtH+7GDybPeZTTbvthy+aRo8cUYxWPjhw1duO2rVu2JzMfr3dzYZF0LzdTmCvk832RPM9hCyaIEy+ZsBBpoRnlqyGXy1FCTzbPeKm0q1WoGnch1c0La9qHqXLxKE4lyqrS0YlKQVTBhJifKGOpfP+nXz5jRv9Yx8HliFwbXOtR1PFn0+lLC1Ayylrb0dn1IqJqHmr1alL4ApnT0inpLa1MVa9kungLQYk7B90SDGiakQ5DgAkBi02djeiqgrJC3A8WiQHFVUZfVBMyRs9yp3McrpPPIhHjXs02m0zspiafT54jDVtGgFJSpoDOqP4YfOU+KO+Cco1xsYaPGBHMdFOTRaBbl9+zyYlcWwZ17Vjw41dOmPAefDDj95+sACaWV+5ynQsLzMZ104NAGoVo/0Oe/eDgrVDUhtl2gl7IOA2Of/FnYgSAXRBPuoI+JS5WDzn11DdramqwyOxarwAmq7Ta3RfqIqZCwWhYZjicHbdDGhoHLeTXfmrHUWwngDaTWWkMe72/JMtn+/43YTIL+pAwwhkAAAAASUVORK5CYII=") 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.2);
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,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAB8BJREFUWAnFWAtsU1UY/s+5XTcYYxgfvERQeQXxNeYLjVFxLVvb2xasKIgSVNQoREVI1GhmfC6ioijiNDo1vBxb19uVtRWUzAQ1+EowOkSQzTBAUJio27r2Hr9TLJTaa7vK4yTtvec///+f7/znf5xzGf2PZnVMKRHUczEJNpgYDSEdPzTB6GdG1EbE2sxk+qqxsW5rrtNAT+/aZLtrkiDdLYhUIcSwQ9KsA7DaAbKdEWOCQBckxwrkOGP0Lf7rTAqrW+vzbT4kk91/1gAB7BqdYlVC0KUAsQuANOKKjwYUNYfff//PdNNZ3O4zqEe/FguZykhUYFGFQKspnBYGNW1LOplUWkaANtvUc3pY5FUAKwewb4jzR0KaN8ikoXrRZs2aVbBr3/6bddKfhHUHAugys+j3eCCwYv9/qflPgFab83ps52ookxZ6OOT3regtsNTJHY45fSO05yGh6wsFsZ1cIVtI035M5Uv0DQFabY77BWOLsNrmQrPi8Xq9vyaEjsXT4pg6VuiRABZfzAVzhwK+T9Lp5emIFru6QCd6CXv4+sRLSizHGpycM+yvayng/S6Do7QIJtZZVXVyOiz/sqDV4XAKweoxsDjUqM1PJ3QsaeVz5+bHtrc2IjWVmky8tKmhYVuy/qMsWOZyXSR0Wo4IDVxRWrIgmfF4vTctWdINF7oJljwQ7dG9lpkzC5PnOgywsrKSU1R/Gz6xo7hPwXT0scsnpkkXEnncjTw6kvZ3vJI8q5Lo5BUV3YaAuFthyjStof6HBP1EPbe3tOweNWpMF0AuGHveuNqtLS375NxxC8rQB7inkOd8wcaGDScKVOo8/fvmLwWOPZFIrDIxFgcYEbtnA9wgk1lZmBgwetrtnqGTbapqNG5Et06ZMhhuYzIal/Ta2tpOlMVnEAOeCqfzfEmLA0SV8KB+bljr9Wbc2ijrujpGwmdxOB+SCrJpckGiu+enT7/85uZM/P375FcjDn6LxsRMycsrPJ5B2PerOLE1mYTleNDvX8k4W4xK8HyZ3XlvJpkym+qJEa1B1VjHRwz7IBM/rBjBNodhxXLJy6N/dbvlSz4nr3xm08J+7QHkyTdI6EssDsftRjJWh2smtmwlyrZ29tBBbplSjHiT6ZyxIHZ1vHQnVBlRArTfaZq2J5kp0zuS+D2w5Hs4/FWj8sxI5bfa1TuF0GtAX4W0Na26uronlceon89FSI5FRPf1HJY4C2e1HUbMRnR5aCguyIf1RC143oW1piZ44Z/zdCFgYXpnYmnJrdg27HL2LW4sxg7A9YYhqthwEmJ99uJHOOXEiMxbNm76qkAX+kps9xSUyXHwzyps02tBv29urqcfGG4fzgKnIYrFMHTajkzbuzcAjBb3zb8ROtajTHqx2Cq8L4IL3JcruEMIxF4cck/niK4IjlV5vYN1NLeMPATDd6DKPBclhfmP5sipdxBSRdKCe/E7PScVEMJxnllszlfgcw/CYk8g4X8OSwbKHY7Lc9Up5aB2MNxvN2eC7UUnJ4DYXm51ON/AqXsuVvpAuFGrVAYUVUD991HBmuStL1eQ2N7hkG1DfqY92J4ze6vI4/EoCI53YcE7EBD3hAL+xVJH0/Llv5tFkRUTtOoiGrbY3ONz0F2MAOnPGG8FQLYRCi7DhP2yVTRnzpy8A391r8TipqNYzkZALEuWlRchpU9BGfbpF8Fi6yar6pjk8UzvBzt7SuM8grbwPBMPwArm37u6JmUSlOPyBLyjfVcdttGNPDfjQ7+/Jp1cU23tXp6fNwkRfTCmi/XydpiOLx0tRvoNWPzOoN+7iQe83u/h2Dvgh7Z0zKk0/afWF+C8VsYVTzigrUodT+6H6ut3IaKvw0KiEYp8pKpqUfJ4unfp16C7meD1Mk3JDprwovbdaLNNP+VQ3/hfKGwFJ+WasL+hwZjryEjY5/vZTObrYJFmznHJzNA+2/S1dI2BsLysUBBDw8qGdOr0Ixz75XCj/2FJOxlNpiyrQ/0CuZmF/b4Jhy2I2ie/qywFqHkAO/BkgJNzWu3OW7GTJZzT/EQV+meL5Veewudg0FhnjJacDIAul2sATlZPw3gavjR8nMBwGCDOofuA+m74o0de3BMMJ+KJwDD9GY2twdGtH+7GDybPeZTTbvthy+aRo8cUYxWPjhw1duO2rVu2JzMfr3dzYZF0LzdTmCvk832RPM9hCyaIEy+ZsBBpoRnlqyGXy1FCTzbPeKm0q1WoGnch1c0La9qHqXLxKE4lyqrS0YlKQVTBhJifKGOpfP+nXz5jRv9Yx8HliFwbXOtR1PFn0+lLC1Ayylrb0dn1IqJqHmr1alL4ApnT0inpLa1MVa9kungLQYk7B90SDGiakQ5DgAkBi02djeiqgrJC3A8WiQHFVUZfVBMyRs9yp3McrpPPIhHjXs02m0zspiafT54jDVtGgFJSpoDOqP4YfOU+KO+Cco1xsYaPGBHMdFOTRaBbl9+zyYlcWwZ17Vjw41dOmPAefDDj95+sACaWV+5ynQsLzMZ104NAGoVo/0Oe/eDgrVDUhtl2gl7IOA2Of/FnYgSAXRBPuoI+JS5WDzn11DdramqwyOxarwAmq7Ta3RfqIqZCwWhYZjicHbdDGhoHLeTXfmrHUWwngDaTWWkMe72/JMtn+/43YTIL+pAwwhkAAAAASUVORK5CYII=") 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();

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

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

5
web/src/lib/helpers.js Normal file
View File

@@ -0,0 +1,5 @@
export const isEmailValid = email => {
let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return validEmailRegex.test (email);
};

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();
});
}
}

5
web/src/setupTests.js Normal file
View File

@@ -0,0 +1,5 @@
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import 'jest-enzyme';
configure({ adapter: new Adapter() });