├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend ├── ChuckBot │ ├── bot.json │ ├── intent.json │ └── slot-type.json ├── MovieBot │ ├── bot.json │ ├── intent.json │ └── slot-type.json ├── ai-lambda │ └── index.js ├── chuckbot-lambda │ ├── Intents.png │ ├── Slots.png │ ├── index.js │ └── package.json ├── deploy.yaml ├── moviebot-lambda │ ├── Intents.png │ ├── Slots.png │ ├── index.js │ └── package.json └── schema.graphql ├── media └── ChatQLv2.png ├── package.json ├── public ├── favicon.ico ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── index.html └── manifest.json └── src ├── App.css ├── App.js ├── App.scss ├── App.test.js ├── components ├── AI │ ├── Bot.js │ ├── DetectCelebs.js │ ├── DetectEntities.js │ ├── DetectLabels.js │ ├── DetectLanguage.js │ ├── DetectSentiment.js │ ├── Dictate.js │ ├── InvokeBot.js │ ├── MenuDropDown.js │ ├── Translate.js │ └── TranslateCard.js ├── AudioCapture.js ├── ConversationBar.js ├── ConvoSideList.js ├── InputBar.js ├── Message.js ├── MessagePane.js ├── Messenger.js ├── SearchResultList.js ├── SideBar.js ├── UserBar.js └── chatapp.js ├── graphql ├── AI │ ├── detectCelebs.js │ ├── detectEntities.js │ ├── detectLabels.js │ ├── detectLanguage.js │ ├── detectSentiment.js │ ├── dictate.js │ ├── invokeBot.js │ └── translate.js ├── mutations.js ├── queries.js ├── schema.json └── subscriptions.js ├── images ├── appsync.png ├── chuck.jpg ├── comprehend.png ├── lex.png ├── lexlogo.png ├── logo.png ├── polly.png ├── rekognition.png └── translate.png ├── index.css ├── index.js ├── index.scss └── registerServiceWorker.js /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | #amplify 4 | amplify/\#current-cloud-backend 5 | amplify/.config/local-* 6 | amplify/backend/amplify-meta.json 7 | aws-exports.js 8 | awsconfiguration.json 9 | 10 | .graphqlconfig.yml 11 | packaged.yaml 12 | 13 | yarn.lock 14 | package-lock.json 15 | 16 | # dependencies 17 | node_modules/ 18 | 19 | # testing 20 | /coverage 21 | 22 | # production 23 | /build 24 | 25 | # misc 26 | .DS_Store 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/aws-appsync-chat-starter-react/issues), or [recently closed](https://github.com/aws-samples/aws-appsync-chat-starter-react/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/aws-appsync-chat-starter-react/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/aws-appsync-chat-starter-react/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatQLv2: An AWS AppSync Chat Starter App written in React 2 | 3 | ## Quicklinks 4 | 5 | - re:Invent 2018 [Session](https://www.youtube.com/watch?v=0H5F0PI2-SU)/[Slides](https://www.slideshare.net/AmazonWebServices/bridging-the-gap-between-real-timeoffline-and-aiml-capabilities-in-modern-serverless-apps-mob310-aws-reinvent-2018) 6 | - [Introduction](#introduction) 7 | - [Getting Started](#getting-started) 8 | - [Prerequisites](#prerequisites) 9 | - [Back End Setup](#back-end-setup) 10 | - [Interacting with Chatbots](#interacting-with-chatbots) 11 | - [Interacting with other AWS AI Services](#interacting-with-other-aws-ai-services) 12 | - [Building, Deploying and Publishing with the Amplify CLI](#building-deploying-and-publishing-with-the-amplify-cli) 13 | - [Back End Setup, Back End and Front End Building, Deploying and Publishing with the Amplify Console](#back-end-setup-back-end-and-front-end-building-deploying-and-publishing-with-the-amplify-console) 14 | - [Clean Up](#clean-up) 15 | 16 | ## Introduction 17 | 18 | This is a Starter React Progressive Web Application (PWA) that uses AWS AppSync to implement offline and real-time capabilities in a chat application with AI/ML features such as image recognition, text-to-speech, language translation, sentiment analysis as well as conversational chatbots developed as part of the re:Invent session [Bridging the Gap Between Real Time/Offline and AI/ML Capabilities in Modern Serverless Apps](https://www.youtube.com/watch?v=0H5F0PI2-SU). In the chat app, users can search for users and messages, have conversations with other users, upload images and exchange messages. The application demonstrates GraphQL Mutations, Queries and Subscriptions with AWS AppSync integrating with other AWS Services: 19 | 20 | ![ChatQL Overview](/media/ChatQLv2.png) 21 | 22 | - Amazon Cognito for user management as well as AuthN/Z 23 | - Amazon DynamoDB with 4x NoSQL Data Sources (Users, Messages, Conversations, ConvoLink) 24 | - Amazon Elasticsearch Data Source for full text search on messages and users 25 | - AWS Lambda as a Serverless Data Source connecting to AI Services 26 | - Amazon Comprehend for sentiment and entity analysis as well as language detection 27 | - Amazon Rekognition for object, scene and celebrity detection on images 28 | - Amazon Lex for conversational chatbots 29 | - Amazon Polly for text-to-speech on messages 30 | - Amazon Translate for language translation 31 | - Amazon S3 for Media Storage 32 | 33 | You can use this for learning purposes or adapt either the application or the GraphQL Schema to meet your needs. 34 | 35 | ## Getting Started 36 | 37 | ### Prerequisites 38 | 39 | - [AWS Account](https://aws.amazon.com/mobile/details) with appropriate permissions to create the related resources 40 | - [NodeJS](https://nodejs.org/en/download/) with [NPM](https://docs.npmjs.com/getting-started/installing-node) 41 | - [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) with output configured as JSON `(pip install awscli --upgrade --user)` 42 | - [AWS Amplify CLI](https://github.com/aws-amplify/amplify-cli) configured for a region where [AWS AppSync](https://docs.aws.amazon.com/general/latest/gr/rande.html) and all other services in use are available `(npm install -g @aws-amplify/cli)` 43 | - [AWS SAM CLI](https://github.com/awslabs/aws-sam-cli) `(pip install --user aws-sam-cli)` 44 | - [Create React App](https://github.com/facebook/create-react-app) `(npm install -g create-react-app)` 45 | - [Install JQ](https://stedolan.github.io/jq/) 46 | - If using Windows, you'll need the [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install-win10) 47 | 48 | ### Back End Setup 49 | 50 | Note: This solution uses Amazon Lex. The service is only supported in us-east-1, us-west-2 and eu-west-1. We recommending launching this entire solution in one of these regions. 51 | 52 | 1. First, clone this repository and navigate to the created folder: 53 | 54 | ```bash 55 | git clone https://github.com/aws-samples/aws-appsync-chat-starter-react.git 56 | cd aws-appsync-chat-starter-react 57 | ``` 58 | 59 | 2. Install the required modules: 60 | 61 | ```bash 62 | npm install 63 | ``` 64 | 65 | 3. Init the directory as an amplify **Javascript** app using the **React** framework: 66 | 67 | ```bash 68 | amplify init 69 | ``` 70 | 71 | Set the region we are deploying resources to: 72 | 73 | ```bash 74 | export AWS_REGION=$(jq -r '.providers.awscloudformation.Region' amplify/#current-cloud-backend/amplify-meta.json) 75 | echo $AWS_REGION 76 | ``` 77 | 78 | Make sure [**ALL**](https://docs.aws.amazon.com/general/latest/gr/rande.html) services are supported in this region or else you'll get errors in the next steps. 79 | 80 | 4. Add an **Amazon Cognito User Pool** auth resource. Use the default configuration. 81 | 82 | ```bash 83 | amplify add auth 84 | ``` 85 | 86 | 5. Add an **AppSync GraphQL** API with **Amazon Cognito User Pool** for the API Authentication. Follow the default options. When prompted with "_Do you have an annotated GraphQL schema?_", select **"YES"** and provide the schema file path `backend/schema.graphql` 87 | 88 | ```bash 89 | amplify add api 90 | ``` 91 | 92 | 6. Add S3 Private Storage for **Content** to the project with the default options. Select private **read/write** access for **Auth users only**: 93 | 94 | ```bash 95 | amplify add storage 96 | ``` 97 | 98 | 7. Now it's time to provision your cloud resources based on the local setup and configured features. When asked to generate code, answer **"NO"** as it would overwrite the current custom files in the `src/graphql` folder. 99 | 100 | ```bash 101 | amplify push 102 | ``` 103 | 104 | Wait for the provisioning to complete. Once done, a `src/aws-exports.js` file with the resources information is created. 105 | 106 | --- 107 | 108 | **_At this point you have an usable serverless chat application with no AI features. The next steps are only needed to deploy and configure the integration with services that provide image recognition, text-to-speech, language translation, sentiment analysis as well as conversational chatbots. From here you can skip to step 13 if there's no interest to setup the AI integration._** 109 | 110 | --- 111 | 112 | 8. Look up the S3 bucket name created for user storage: 113 | 114 | ```bash 115 | export USER_FILES_BUCKET=$(sed -n 's/.*"aws_user_files_s3_bucket": "\(.*\)".*/\1/p' src/aws-exports.js) 116 | echo $USER_FILES_BUCKET 117 | ``` 118 | 119 | 9. Retrieve the API ID of your AppSync GraphQL endpoint 120 | 121 | ```bash 122 | export GRAPHQL_API_ID=$(jq -r '.api[(.api | keys)[0]].output.GraphQLAPIIdOutput' ./amplify/#current-cloud-backend/amplify-meta.json) 123 | echo $GRAPHQL_API_ID 124 | ``` 125 | 126 | 10. Retrieve the project's deployment bucket and stackname . It will be used for packaging and deployment with SAM 127 | 128 | ```bash 129 | export DEPLOYMENT_BUCKET_NAME=$(jq -r '.providers.awscloudformation.DeploymentBucketName' ./amplify/#current-cloud-backend/amplify-meta.json) 130 | export STACK_NAME=$(jq -r '.providers.awscloudformation.StackName' ./amplify/#current-cloud-backend/amplify-meta.json) 131 | echo $DEPLOYMENT_BUCKET_NAME 132 | echo $STACK_NAME 133 | ``` 134 | 135 | 11. Now we need to deploy 3 Lambda functions (one for AppSync and two for Lex) and configure the AppSync Resolvers to use Lambda accordingly. First, we install the npm dependencies for each lambda function. We then package and deploy the changes with SAM. 136 | 137 | ```bash 138 | cd ./backend/chuckbot-lambda; npm install; cd ../.. 139 | cd ./backend/moviebot-lambda; npm install; cd ../.. 140 | sam package --template-file ./backend/deploy.yaml --s3-bucket $DEPLOYMENT_BUCKET_NAME --output-template-file packaged.yaml 141 | export STACK_NAME_AIML="$STACK_NAME-extra-aiml" 142 | sam deploy --template-file ./packaged.yaml --stack-name $STACK_NAME_AIML --capabilities CAPABILITY_IAM --parameter-overrides appSyncAPI=$GRAPHQL_API_ID s3Bucket=$USER_FILES_BUCKET --region $AWS_REGION 143 | ``` 144 | 145 | Wait for the stack to finish deploying then retrieve the functions' ARN 146 | 147 | ```bash 148 | export CHUCKBOT_FUNCTION_ARN=$(aws cloudformation describe-stacks --stack-name $STACK_NAME_AIML --query "Stacks[0].Outputs" --region $AWS_REGION --output json | jq -r '.[] | select(.OutputKey == "ChuckBotFunction") | .OutputValue') 149 | export MOVIEBOT_FUNCTION_ARN=$(aws cloudformation describe-stacks --stack-name $STACK_NAME_AIML --query "Stacks[0].Outputs" --region $AWS_REGION --output json | jq -r '.[] | select(.OutputKey == "MovieBotFunction") | .OutputValue') 150 | echo $CHUCKBOT_FUNCTION_ARN 151 | echo $MOVIEBOT_FUNCTION_ARN 152 | ``` 153 | 154 | 12. Let's set up Lex. We will create 2 chatbots: ChuckBot and MovieBot. Execute the following commands to add permissions so Lex can invoke the chatbot related functions: 155 | 156 | ```bash 157 | aws lambda add-permission --statement-id Lex --function-name $CHUCKBOT_FUNCTION_ARN --action lambda:\* --principal lex.amazonaws.com --region $AWS_REGION 158 | aws lambda add-permission --statement-id Lex --function-name $MOVIEBOT_FUNCTION_ARN --action lambda:\* --principal lex.amazonaws.com --region $AWS_REGION 159 | ``` 160 | 161 | Update the bots intents with the Lambda ARN: 162 | 163 | ```bash 164 | jq '.fulfillmentActivity.codeHook.uri = $arn' --arg arn $CHUCKBOT_FUNCTION_ARN backend/ChuckBot/intent.json -M > tmp.txt ; cp tmp.txt backend/ChuckBot/intent.json; rm tmp.txt 165 | jq '.fulfillmentActivity.codeHook.uri = $arn' --arg arn $MOVIEBOT_FUNCTION_ARN backend/MovieBot/intent.json -M > tmp.txt ; cp tmp.txt backend/MovieBot/intent.json; rm tmp.txt 166 | ``` 167 | 168 | And, deploy the slot types, intents and bots: 169 | 170 | ```bash 171 | aws lex-models put-slot-type --cli-input-json file://backend/ChuckBot/slot-type.json --region $AWS_REGION 172 | aws lex-models put-intent --cli-input-json file://backend/ChuckBot/intent.json --region $AWS_REGION 173 | aws lex-models put-bot --cli-input-json file://backend/ChuckBot/bot.json --region $AWS_REGION 174 | aws lex-models put-slot-type --cli-input-json file://backend/MovieBot/slot-type.json --region $AWS_REGION 175 | aws lex-models put-intent --cli-input-json file://backend/MovieBot/intent.json --region $AWS_REGION 176 | aws lex-models put-bot --cli-input-json file://backend/MovieBot/bot.json --region $AWS_REGION 177 | ``` 178 | 179 | 13. Finally, execute the following command to install your project package dependencies and run the application locally: 180 | 181 | ```bash 182 | amplify serve 183 | ``` 184 | 185 | 14. Access your ChatQLv2 app at http://localhost:3000. Sign up at least 2 different users, authenticate with each user to get them registered in the backend Users table, then search for new users to start a conversation and test real-time/offline messaging as well as other features using different devices or browsers. 186 | 187 | ### Interacting with Chatbots 188 | 189 | _The chatbots retrieve information online via API calls from Lambda to [The Movie Database (TMDb)](https://www.themoviedb.org/) (MovieBot, which is based on this [chatbot sample](https://github.com/aws-samples/aws-lex-convo-bot-example)) and [chucknorris.io ](https://api.chucknorris.io/) (ChuckBot)_ 190 | 191 | 1. In order to initiate or respond to a chatbot conversation, you need to start the message with either `@chuckbot` or `@moviebot` to trigger or respond to the specific bot, for example: 192 | 193 | - _@chuckbot Give me a Chuck Norris fact_ 194 | - _@moviebot Tell me about a movie_ 195 | 196 | 2. Each subsequent response needs to start with the bot handle (@chuckbot or @moviebot) so the app can detect the message is directed to Lex and not to the other user in the same conversation. Both users will be able to view Lex chatbot responses in real-time powered by GraphQL subscriptions. 197 | 3. Alternatively you can start a chatbot conversation from the message drop-down menu: 198 | 199 | - Just selecting `ChuckBot` will display options for further interaction 200 | - Send a message with a nothing but a movie name and selecting `MovieBot` subsequently will retrieve the details about the movie 201 | 202 | ### Interacting with other AWS AI Services 203 | 204 | 1. Click or select uploaded images to trigger Amazon Rekognition object, scene and celebrity detection. 205 | 2. From the drop-down menu, select LISTEN -> TEXT TO SPEECH to trigger Amazon Polly and listen to messages in different voices based on the message automatically detected source language (supported languages: English, Mandarin, Portuguese, French and Spanish). 206 | 3. To perform entity and sentiment analysis on messages via Amazon Comprehend, select ANALYZE -> SENTIMENT from the drop-down menu. 207 | 4. To translate the message select the desired language under TRANSLATE in the drop-down menu (supported languages: English, Mandarin, Portuguese, French and Spanish). In the translation pane, click on the microphone icon to listen to the translated message. 208 | 209 | ## Building, Deploying and Publishing with the Amplify CLI 210 | 211 | 1. Execute `amplify add hosting` from the project's root folder and follow the prompts to create an S3 bucket (DEV) and/or a CloudFront distribution (PROD). 212 | 213 | 2. Build, deploy, upload and publish the application with a single command: 214 | 215 | ```bash 216 | amplify publish 217 | ``` 218 | 219 | 3. If you are deploying a CloudFront distribution, be mindful it needs to be replicated across all points of presence globally and it might take up to 15 minutes to do so. 220 | 221 | 4. Access your public ChatQL application using the S3 Website Endpoint URL or the CloudFront URL returned by the `amplify publish` command. Share the link with friends, sign up some users, and start creating conversations, uploading images, translating, executing text-to-speech in different languages, performing sentiment analysis and exchanging messages. Be mindful PWAs require SSL, in order to test PWA functionality access the CloudFront URL (HTTPS) from a mobile device and add the site to the mobile home screen. 222 | 223 | ## Back End Setup, Back End and Front End Building, Deploying and Publishing with the Amplify Console 224 | 225 | (More info [here](https://docs.aws.amazon.com/amplify/latest/userguide/deploy-backend.html)) 226 | 227 | 1. Fork this repository into your own GitHub account and clone it 228 | 2. Repeat Steps 3 to 6 from the [Back End Setup](#back-end-setup) in the previous section. Do not perform step 7 (`amplify push`). 229 | 3. Commit the changes to your forked repository. A new folder `amplify` will be commited with the project details. 230 | 4. Connect your repository to the [Amplify Console](https://console.aws.amazon.com/amplify/home?#/create) as per the instructions [here](https://docs.aws.amazon.com/amplify/latest/userguide/getting-started.html), making sure the name of the branch in your repository matches the name of the environment configured on `amplify init` (i.e. master). When prompted with "_We detected a backend created with the Amplify Framework. Would you like Amplify Console to deploy these resources with your frontend?_", select **"YES"** and provide or create an IAM role with appropriate permissions to build the backend resources 231 | 5. Wait for the build, deployment and verification steps 232 | 233 | --- 234 | 235 | **_At this point you have an usable serverless chat application with no AI features. The next steps are only needed to deploy and configure the integration with services that provide image recognition, text-to-speech, language translation, sentiment analysis as well as conversational chatbots. From here you can skip to step 8 if there's no interest to setup the AI integration._** 236 | 237 | --- 238 | 239 | 7. Now perform steps 7 to 12 from the [Back End Setup](#back-end-setup) 240 | 8. Access your app from the hosted site generated by the Amplify Console(https://master.xxxxxxxx.amplifyapp.com) 241 | 242 | ## Clean Up 243 | 244 | To clean up the project, you can simply delete the bots, delete the stack created by the SAM CLI: 245 | 246 | ```bash 247 | aws lex-models delete-bot --name `jq -r .name backend/ChuckBot/bot.json` --region $AWS_REGION 248 | aws lex-models delete-bot --name `jq -r .name backend/MovieBot/bot.json` --region $AWS_REGION 249 | aws lex-models delete-intent --name `jq -r .name backend/ChuckBot/intent.json` --region $AWS_REGION 250 | aws lex-models delete-intent --name `jq -r .name backend/MovieBot/intent.json` --region $AWS_REGION 251 | aws lex-models delete-slot-type --name `jq -r .name backend/ChuckBot/slot-type.json` --region $AWS_REGION 252 | aws lex-models delete-slot-type --name `jq -r .name backend/MovieBot/slot-type.json` --region $AWS_REGION 253 | 254 | aws cloudformation delete-stack --stack-name $STACK_NAME_AIML --region $AWS_REGION 255 | ``` 256 | 257 | and use: 258 | 259 | ```bash 260 | amplify delete 261 | ``` 262 | 263 | to delete the resources created by the Amplify CLI. 264 | -------------------------------------------------------------------------------- /backend/ChuckBot/bot.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChuckBot", 3 | "intents": [ 4 | { 5 | "intentName": "ChuckNorrisFacts", 6 | "intentVersion": "1" 7 | } 8 | ], 9 | "clarificationPrompt": { 10 | "messages": [ 11 | { 12 | "contentType": "PlainText", 13 | "content": "Please try something like: \\[@chuckbot Give me a Chuck Norris fact\\]" 14 | } 15 | ], 16 | "maxAttempts": 2 17 | }, 18 | "abortStatement": { 19 | "messages": [ 20 | { 21 | "contentType": "PlainText", 22 | "content": "Sorry, I am not able to assist at this time" 23 | } 24 | ] 25 | }, 26 | "processBehavior": "BUILD", 27 | "voiceId": "Matthew", 28 | "childDirected": false, 29 | "locale": "en-US", 30 | "idleSessionTTLInSeconds": 300, 31 | "createVersion": true 32 | } 33 | -------------------------------------------------------------------------------- /backend/ChuckBot/intent.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChuckNorrisFacts", 3 | "createVersion": true, 4 | "fulfillmentActivity": { 5 | "type": "CodeHook", 6 | "codeHook": { 7 | "uri": "", 8 | "messageVersion": "1.0" 9 | } 10 | }, 11 | "sampleUtterances": [ 12 | "Give me a Chuck Norris fact", 13 | "Give me a Chuck Norris {FactType} fact" 14 | ], 15 | "slots": [ 16 | { 17 | "name": "FactType", 18 | "slotConstraint": "Required", 19 | "slotType": "FactTypes", 20 | "slotTypeVersion": "1", 21 | "valueElicitationPrompt": { 22 | "messages": [ 23 | { 24 | "contentType": "PlainText", 25 | "content": "What kind of fact? (Software Development, Movies, Science, Music or Food)" 26 | } 27 | ], 28 | "maxAttempts": 2, 29 | "responseCard": "{\"version\":1,\"contentType\":\"application/vnd.amazonaws.card.generic\",\"genericAttachments\":[{\"imageUrl\":\"https://i.imgflip.com/1octba.jpg\",\"subTitle\":\"Choose the category:\",\"title\":\"Chuck Norris Facts\",\"buttons\":[{\"text\":\"Software Development\",\"value\":\"Software Development\"},{\"text\":\"Science\",\"value\":\"Science\"},{\"text\":\"Movies\",\"value\":\"Movies\"},{\"text\":\"Music\",\"value\":\"Music\"},{\"text\":\"Food\",\"value\":\"Food\"}]}]}" 30 | }, 31 | "priority": 1, 32 | "sampleUtterances": [] 33 | } 34 | ], 35 | "conclusionStatement": { 36 | "messages": [ 37 | { 38 | "contentType": "PlainText", 39 | "content": "For more facts, just click on my name in the dropdown.", 40 | "groupNumber": 1 41 | } 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/ChuckBot/slot-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FactTypes", 3 | "description": "Available Chuck Norris Facts", 4 | "createVersion": true, 5 | "enumerationValues": [ 6 | { 7 | "value": "Travel", 8 | "synonyms": [] 9 | }, 10 | { 11 | "value": "Money", 12 | "synonyms": [] 13 | }, 14 | { 15 | "value": "Software Development", 16 | "synonyms": [] 17 | }, 18 | { 19 | "value": "Celebrities", 20 | "synonyms": [] 21 | }, 22 | { 23 | "value": "Music", 24 | "synonyms": [] 25 | }, 26 | { 27 | "value": "Science", 28 | "synonyms": [] 29 | }, 30 | { 31 | "value": "Movies", 32 | "synonyms": [] 33 | }, 34 | { 35 | "value": "History", 36 | "synonyms": [] 37 | }, 38 | { 39 | "value": "Career", 40 | "synonyms": [] 41 | }, 42 | { 43 | "value": "Food", 44 | "synonyms": [] 45 | } 46 | ], 47 | "valueSelectionStrategy": "ORIGINAL_VALUE" 48 | } 49 | -------------------------------------------------------------------------------- /backend/MovieBot/bot.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MovieBot", 3 | "intents": [ 4 | { 5 | "intentName": "Movies", 6 | "intentVersion": "1" 7 | } 8 | ], 9 | "voiceId": "Matthew", 10 | "processBehavior": "BUILD", 11 | "createVersion": true, 12 | "childDirected": false, 13 | "locale": "en-US", 14 | "idleSessionTTLInSeconds": 300, 15 | "clarificationPrompt": { 16 | "messages": [ 17 | { 18 | "contentType": "PlainText", 19 | "content": "Please try something like: \\[@moviebot Tell me about a movie\\], \\[@moviebot Tell me about \\[Movie\\]\\], \\[@moviebot \\[Movie\\] \\[Plot\\]\\], \\[@moviebot \\[Movie\\] \\[Year\\]\\] or even just \\[@moviebot \\[Movie\\]\\]" 20 | } 21 | ], 22 | "maxAttempts": 5 23 | }, 24 | "abortStatement": { 25 | "messages": [ 26 | { 27 | "contentType": "PlainText", 28 | "content": "Sorry, I could not understand. Goodbye." 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/MovieBot/intent.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Movies", 3 | "createVersion": true, 4 | "fulfillmentActivity": { 5 | "type": "CodeHook", 6 | "codeHook": { 7 | "uri": "", 8 | "messageVersion": "1.0" 9 | } 10 | }, 11 | "sampleUtterances": [ 12 | "Tell me about a movie", 13 | "Tell me about {name}", 14 | "Tell me {summary} about {name}", 15 | "{name}", 16 | "{name} {summary}" 17 | ], 18 | "slots": [ 19 | { 20 | "name": "name", 21 | "slotConstraint": "Required", 22 | "slotType": "AMAZON.Movie", 23 | "valueElicitationPrompt": { 24 | "messages": [ 25 | { 26 | "contentType": "PlainText", 27 | "content": "What movie do you want to know about?" 28 | } 29 | ], 30 | "maxAttempts": 2 31 | }, 32 | "priority": 1, 33 | "sampleUtterances": [] 34 | }, 35 | { 36 | "name": "summary", 37 | "slotConstraint": "Optional", 38 | "slotType": "MovieDetails", 39 | "slotTypeVersion": "1", 40 | "valueElicitationPrompt": { 41 | "messages": [ 42 | { 43 | "contentType": "PlainText", 44 | "content": "What information are you looking for? (Year, Plot, All)" 45 | } 46 | ], 47 | "maxAttempts": 2, 48 | "responseCard": "{\"version\":1,\"contentType\":\"application/vnd.amazonaws.card.generic\",\"genericAttachments\":[]}" 49 | }, 50 | "priority": 2, 51 | "sampleUtterances": [] 52 | } 53 | ], 54 | "conclusionStatement": { 55 | "messages": [ 56 | { 57 | "contentType": "PlainText", 58 | "content": "Hope that was useful! Click the link to view more info on IMDB. For another movie, just type \\"\\@moviebot \\<Movie Name\\>\\" or send a movie name and click on my name in the dropdown", 59 | "groupNumber": 1 60 | } 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/MovieBot/slot-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MovieDetails", 3 | "description": "Movie Details", 4 | "createVersion": true, 5 | "enumerationValues": [ 6 | { 7 | "value": "Year", 8 | "synonyms": [] 9 | }, 10 | { 11 | "value": "Voting", 12 | "synonyms": [] 13 | }, 14 | { 15 | "value": "Plot", 16 | "synonyms": [] 17 | }, 18 | { 19 | "value": "Director", 20 | "synonyms": [] 21 | }, 22 | { 23 | "value": "Actors", 24 | "synonyms": [] 25 | } 26 | ], 27 | "valueSelectionStrategy": "ORIGINAL_VALUE" 28 | } 29 | -------------------------------------------------------------------------------- /backend/ai-lambda/index.js: -------------------------------------------------------------------------------- 1 | let AWS = require("aws-sdk"); 2 | let rekognition = new AWS.Rekognition(); 3 | let lex = new AWS.LexRuntime(); 4 | let polly = new AWS.Polly(); 5 | let translate = new AWS.Translate(); 6 | let comprehend = new AWS.Comprehend(); 7 | let s3 = new AWS.S3(); 8 | 9 | exports.handler = (event, context, callback) => { 10 | console.log(event); 11 | switch (event.field) { 12 | case "lex": 13 | const lexparams = { 14 | botAlias: "$LATEST", 15 | botName: event.arguments.bot, 16 | inputText: event.arguments.text, 17 | userId: event.arguments.sender 18 | }; 19 | lex.postText(lexparams, function(err, data) { 20 | if (err) { 21 | console.error("Error JSON: ", JSON.stringify(err, null, 2)); 22 | callback(err); 23 | } else { 24 | let result = { 25 | bot: event.arguments.bot, 26 | text: event.arguments.text, 27 | response: data.message 28 | }; 29 | callback(null, result); 30 | } 31 | }); 32 | break; 33 | case "rekognition-labels": 34 | const labelRekogparams = { 35 | Image: { 36 | S3Object: { 37 | Bucket: event.arguments.bucket, 38 | Name: event.arguments.key 39 | } 40 | }, 41 | MaxLabels: 5, 42 | MinConfidence: 70 43 | }; 44 | rekognition.detectLabels(labelRekogparams, function(err, data) { 45 | if (err) { 46 | console.error("Error JSON: ", JSON.stringify(err, null, 2)); 47 | callback(err); 48 | } else { 49 | // successful response 50 | let result = { 51 | bucket: event.arguments.bucket, 52 | key: event.arguments.key, 53 | response: data.Labels 54 | }; 55 | callback(null, result); 56 | } 57 | }); 58 | break; 59 | case "rekognition-celebs": 60 | const celebRekogparams = { 61 | Image: { 62 | S3Object: { 63 | Bucket: event.arguments.bucket, 64 | Name: event.arguments.key 65 | } 66 | } 67 | }; 68 | rekognition.recognizeCelebrities(celebRekogparams, function(err, data) { 69 | if (err) { 70 | console.error("Error JSON: ", JSON.stringify(err, null, 2)); 71 | callback(err); 72 | } else { 73 | // successful response 74 | let result = { 75 | bucket: event.arguments.bucket, 76 | key: event.arguments.key, 77 | response: data.CelebrityFaces 78 | }; 79 | callback(null, result); 80 | } 81 | }); 82 | break; 83 | case "polly": 84 | const pollyparams = { 85 | OutputFormat: "mp3", 86 | Text: event.arguments.text, 87 | VoiceId: event.arguments.voice 88 | }; 89 | polly 90 | .synthesizeSpeech(pollyparams) 91 | .on("success", function(response) { 92 | console.log("Polly Synthesize Speech Success!"); 93 | let data = response.data; 94 | let audioStream = data.AudioStream; 95 | send2S3(audioStream, event.arguments.key, event.arguments.bucket); 96 | }) 97 | .on("error", function(err) { 98 | console.error("Error JSON: ", JSON.stringify(err, null, 2)); 99 | callback(err); 100 | }) 101 | .send(); 102 | let url = getUrl(event.arguments.key, event.arguments.bucket); 103 | console.log("Signed URL: " + url); 104 | let result = { 105 | bucket: event.arguments.bucket, 106 | key: event.arguments.key + ".mp3", 107 | response: url 108 | }; 109 | callback(null, result); 110 | break; 111 | case "translate": 112 | const translateParams = { 113 | SourceLanguageCode: "auto", 114 | TargetLanguageCode: event.arguments.language, 115 | Text: event.arguments.text 116 | }; 117 | translate.translateText(translateParams, function(err, data) { 118 | if (err) { 119 | console.error("Error JSON: ", JSON.stringify(err, null, 2)); 120 | callback(err); 121 | } else { 122 | // successful response 123 | let result = { 124 | response: data 125 | }; 126 | callback(null, result); 127 | } 128 | }); 129 | break; 130 | case "comprehend-language": 131 | const compLangParams = { 132 | Text: event.arguments.text 133 | }; 134 | comprehend.detectDominantLanguage(compLangParams, function(err, data) { 135 | if (err) { 136 | console.error("Error JSON: ", JSON.stringify(err, null, 2)); 137 | callback(err); 138 | } else { 139 | // successful response 140 | let result = { 141 | response: data.Languages 142 | }; 143 | callback(null, result); 144 | } 145 | }); 146 | break; 147 | case "comprehend-sentiment": 148 | const compSentParams = { 149 | LanguageCode: event.arguments.language, 150 | Text: event.arguments.text 151 | }; 152 | comprehend.detectSentiment(compSentParams, function(err, data) { 153 | if (err) { 154 | console.error("Error JSON: ", JSON.stringify(err, null, 2)); 155 | callback(err); 156 | } else { 157 | // successful response 158 | let result = { 159 | response: data 160 | }; 161 | callback(null, result); 162 | } 163 | }); 164 | break; 165 | case "comprehend-entities": 166 | const compEntParams = { 167 | LanguageCode: event.arguments.language, 168 | Text: event.arguments.text 169 | }; 170 | comprehend.detectEntities(compEntParams, function(err, data) { 171 | if (err) { 172 | console.error("Error JSON: ", JSON.stringify(err, null, 2)); 173 | callback(err); 174 | } else { 175 | // successful response 176 | let result = { 177 | response: data.Entities 178 | }; 179 | callback(null, result); 180 | } 181 | }); 182 | break; 183 | default: 184 | callback("Unknown field, unable to resolve" + event.field, null); 185 | break; 186 | } 187 | }; 188 | 189 | function send2S3(data, key, bucket) { 190 | console.log("Valid Buffer"); 191 | let params = { 192 | Bucket: bucket, 193 | Key: key + ".mp3", 194 | Body: data 195 | }; 196 | s3.putObject(params) 197 | .on("success", function(response) { 198 | console.log("S3 Put Success!"); 199 | }) 200 | .on("complete", function() { 201 | //let url = getUrl(id); 202 | console.log("S3 Put Complete!"); 203 | }) 204 | .on("error", function(response) { 205 | console.log(response); 206 | }) 207 | .send(); 208 | //return url; 209 | } 210 | 211 | function getUrl(key, bucket) { 212 | let params = { 213 | Bucket: bucket, 214 | Key: key + ".mp3" 215 | }; 216 | let url = s3.getSignedUrl("getObject", params); 217 | console.log("File " + params.Key + " saved to S3"); 218 | return url; 219 | } 220 | -------------------------------------------------------------------------------- /backend/chuckbot-lambda/Intents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/backend/chuckbot-lambda/Intents.png -------------------------------------------------------------------------------- /backend/chuckbot-lambda/Slots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/backend/chuckbot-lambda/Slots.png -------------------------------------------------------------------------------- /backend/chuckbot-lambda/index.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | let ret; 3 | 4 | // Close dialog with the customer, reporting fulfillmentState of Failed or Fulfilled ("Thanks, your pizza will arrive in 20 minutes") 5 | function close(sessionAttributes, fulfillmentState, message) { 6 | return { 7 | sessionAttributes, 8 | dialogAction: { 9 | type: "Close", 10 | fulfillmentState, 11 | message 12 | } 13 | }; 14 | } 15 | 16 | // --------------- Events ----------------------- 17 | 18 | async function dispatch(intentRequest, callback) { 19 | console.log( 20 | "request received for userId=${intentRequest.userId}, intentName=${intentRequest.currentIntent.intentName}" 21 | ); 22 | const sessionAttributes = intentRequest.sessionAttributes; 23 | const slots = intentRequest.currentIntent.slots; 24 | let response = "", 25 | url = "", 26 | category = ""; 27 | const baseUrl = "https://api.chucknorris.io/jokes/random?category="; 28 | let factType = slots.FactType; 29 | console.log(`request received for Slots=${factType}`); 30 | 31 | if (factType.includes("chuckbot")) { 32 | let temp = factType.replace("chuckbot", ""); 33 | factType = temp; 34 | } 35 | 36 | try { 37 | switch (factType) { 38 | case "Software Development": 39 | case "software development": 40 | case "development": 41 | case "developer": 42 | case " Software Development": 43 | case " software development": 44 | case " development": 45 | case " developer": 46 | category = "dev"; 47 | url = baseUrl.concat(category); 48 | ret = await axios(url); 49 | response = ret.data.value.trim(); 50 | callback( 51 | close(sessionAttributes, "Fulfilled", { 52 | contentType: "PlainText", 53 | content: ` ${response}` 54 | }) 55 | ); 56 | break; 57 | case "Music": 58 | case "music": 59 | case " Music": 60 | case " music": 61 | category = "music"; 62 | url = baseUrl.concat(category); 63 | ret = await axios(url); 64 | response = ret.data.value.trim(); 65 | callback( 66 | close(sessionAttributes, "Fulfilled", { 67 | contentType: "PlainText", 68 | content: ` ${response}` 69 | }) 70 | ); 71 | break; 72 | case "Science": 73 | case "science": 74 | case " Science": 75 | case " science": 76 | category = "science"; 77 | url = baseUrl.concat(category); 78 | ret = await axios(url); 79 | response = ret.data.value.trim(); 80 | callback( 81 | close(sessionAttributes, "Fulfilled", { 82 | contentType: "PlainText", 83 | content: ` ${response}` 84 | }) 85 | ); 86 | break; 87 | case "Movies": 88 | case "movies": 89 | case " Movies": 90 | case " movies": 91 | category = "movie"; 92 | url = baseUrl.concat(category); 93 | ret = await axios(url); 94 | response = ret.data.value.trim(); 95 | callback( 96 | close(sessionAttributes, "Fulfilled", { 97 | contentType: "PlainText", 98 | content: ` ${response}` 99 | }) 100 | ); 101 | break; 102 | case "Food": 103 | case "food": 104 | case " Food": 105 | case " food": 106 | category = "food"; 107 | url = baseUrl.concat(category); 108 | ret = await axios(url); 109 | response = ret.data.value.trim(); 110 | callback( 111 | close(sessionAttributes, "Fulfilled", { 112 | contentType: "PlainText", 113 | content: ` ${response}` 114 | }) 115 | ); 116 | break; 117 | default: 118 | response = "Unsupported fact, try again: " + factType; 119 | callback( 120 | close(sessionAttributes, "Fulfilled", { 121 | contentType: "PlainText", 122 | content: ` ${response}` 123 | }) 124 | ); 125 | break; 126 | } 127 | } catch (err) { 128 | console.log(err); 129 | return err; 130 | } 131 | } 132 | 133 | // --------------- Main handler ----------------------- 134 | 135 | // Route the incoming request based on intent. 136 | // The JSON body of the request is provided in the event slot. 137 | exports.lambda_handler = (event, context, callback) => { 138 | try { 139 | dispatch(event, response => { 140 | callback(null, response); 141 | }); 142 | } catch (err) { 143 | callback(err); 144 | } 145 | }; 146 | -------------------------------------------------------------------------------- /backend/chuckbot-lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello_world", 3 | "version": "1.0.0", 4 | "description": "hello world sample for NodeJS", 5 | "main": "src/index.js", 6 | "repository": "https://github.com/aws-samples/cookiecutter-aws-sam-hello-nodejs", 7 | "author": "SAM CLI", 8 | "license": "MIT", 9 | "dependencies": { 10 | "axios": "^0.18.0" 11 | }, 12 | "scripts": { 13 | "test": "mocha tests/unit/" 14 | }, 15 | "devDependencies": { 16 | "chai": "^4.1.2", 17 | "mocha": "^5.1.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/deploy.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: "AWS::Serverless-2016-10-31" 3 | Description: ChatQL 4 | Parameters: 5 | s3Bucket: 6 | Type: String 7 | Description: S3 bucket associated with this project 8 | appSyncAPI: 9 | Type: String 10 | Description: GraphQL API associated with this project 11 | Outputs: 12 | ChatQLFunction: 13 | Description: The GraphQL Lambda Data Source 14 | Value: !GetAtt ChatQLAIProxy.Arn 15 | ChuckBotFunction: 16 | Description: ChuckBot Lex Chatbot Lambda 17 | Value: !GetAtt ChuckBot.Arn 18 | MovieBotFunction: 19 | Description: MovieBot Lex Chatbot Lambda 20 | Value: !GetAtt MovieBot.Arn 21 | Resources: 22 | ChatQLAIProxy: 23 | Type: "AWS::Serverless::Function" 24 | Properties: 25 | Handler: ai-lambda/index.handler 26 | Runtime: nodejs10.x 27 | CodeUri: . 28 | Description: "" 29 | MemorySize: 1024 30 | Timeout: 10 31 | Policies: 32 | - AmazonRekognitionReadOnlyAccess 33 | - AmazonPollyReadOnlyAccess 34 | - ComprehendReadOnly 35 | - AWSLambdaBasicExecutionRole 36 | - AmazonLexRunBotsOnly 37 | - TranslateReadOnly 38 | - S3CrudPolicy: 39 | BucketName: !Ref s3Bucket 40 | MovieBot: 41 | Type: "AWS::Serverless::Function" 42 | Properties: 43 | Handler: moviebot-lambda/index.handler 44 | Runtime: nodejs10.x 45 | CodeUri: . 46 | Description: "" 47 | MemorySize: 128 48 | Timeout: 3 49 | ChuckBot: 50 | Type: 'AWS::Serverless::Function' 51 | Properties: 52 | Handler: chuckbot-lambda/index.lambda_handler 53 | Runtime: nodejs10.x 54 | CodeUri: . 55 | Description: '' 56 | MemorySize: 128 57 | Timeout: 3 58 | awsAppSyncServiceRole: 59 | Type: "AWS::IAM::Role" 60 | Properties: 61 | AssumeRolePolicyDocument: 62 | Version: "2012-10-17" 63 | Statement: 64 | - 65 | Effect: "Allow" 66 | Principal: 67 | Service: 68 | - "appsync.amazonaws.com" 69 | Action: 70 | - "sts:AssumeRole" 71 | Path: "/" 72 | lambdaAccessPolicy: 73 | Type: "AWS::IAM::Policy" 74 | Properties: 75 | PolicyName: "lambda-access" 76 | PolicyDocument: 77 | Version: "2012-10-17" 78 | Statement: 79 | - 80 | Effect: "Allow" 81 | Action: "lambda:InvokeFunction" 82 | Resource: 83 | - !GetAtt ChatQLAIProxy.Arn 84 | Roles: 85 | - 86 | Ref: "awsAppSyncServiceRole" 87 | ChatQLAIProxyDataSource: 88 | Type: "AWS::AppSync::DataSource" 89 | Properties: 90 | ApiId: !Ref appSyncAPI 91 | Name: "AIproxy" 92 | Description: "AIproxy Lambda data source" 93 | Type: "AWS_LAMBDA" 94 | ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn 95 | LambdaConfig: 96 | LambdaFunctionArn: !GetAtt ChatQLAIProxy.Arn 97 | queryDetectLabelsResolver: 98 | Type: "AWS::AppSync::Resolver" 99 | Properties: 100 | ApiId: !Ref appSyncAPI 101 | TypeName: "Query" 102 | FieldName: "detectLabels" 103 | DataSourceName: !GetAtt ChatQLAIProxyDataSource.Name 104 | RequestMappingTemplate: | 105 | { 106 | "version" : "2017-02-28", 107 | "operation": "Invoke", 108 | "payload": { 109 | "field": "rekognition-labels", 110 | "arguments": $utils.toJson($context.args) 111 | } 112 | } 113 | ResponseMappingTemplate: | 114 | $util.toJson($context.result) 115 | queryDetectCelebsResolver: 116 | Type: "AWS::AppSync::Resolver" 117 | Properties: 118 | ApiId: !Ref appSyncAPI 119 | TypeName: "Query" 120 | FieldName: "detectCelebs" 121 | DataSourceName: !GetAtt ChatQLAIProxyDataSource.Name 122 | RequestMappingTemplate: | 123 | { 124 | "version" : "2017-02-28", 125 | "operation": "Invoke", 126 | "payload": { 127 | "field": "rekognition-celebs", 128 | "arguments": $utils.toJson($context.args) 129 | } 130 | } 131 | ResponseMappingTemplate: | 132 | $util.toJson($context.result) 133 | queryInvokeBotResolver: 134 | Type: "AWS::AppSync::Resolver" 135 | Properties: 136 | ApiId: !Ref appSyncAPI 137 | TypeName: "Query" 138 | FieldName: "invokeBot" 139 | DataSourceName: !GetAtt ChatQLAIProxyDataSource.Name 140 | RequestMappingTemplate: | 141 | { 142 | "version" : "2017-02-28", 143 | "operation": "Invoke", 144 | "payload": { 145 | "field": "lex", 146 | "arguments": { 147 | "bot": "$context.arguments.bot", 148 | "sender": "$context.identity.sub", 149 | "text": "$context.arguments.text" 150 | } 151 | } 152 | } 153 | ResponseMappingTemplate: | 154 | $util.toJson($context.result) 155 | queryDictateResolver: 156 | Type: "AWS::AppSync::Resolver" 157 | Properties: 158 | ApiId: !Ref appSyncAPI 159 | TypeName: "Query" 160 | FieldName: "dictate" 161 | DataSourceName: !GetAtt ChatQLAIProxyDataSource.Name 162 | RequestMappingTemplate: | 163 | { 164 | "version" : "2017-02-28", 165 | "operation": "Invoke", 166 | "payload": { 167 | "field": "polly", 168 | "arguments": $utils.toJson($context.args) 169 | } 170 | } 171 | ResponseMappingTemplate: | 172 | $util.toJson($context.result) 173 | queryTranslateResolver: 174 | Type: "AWS::AppSync::Resolver" 175 | Properties: 176 | ApiId: !Ref appSyncAPI 177 | TypeName: "Query" 178 | FieldName: "translate" 179 | DataSourceName: !GetAtt ChatQLAIProxyDataSource.Name 180 | RequestMappingTemplate: | 181 | { 182 | "version" : "2017-02-28", 183 | "operation": "Invoke", 184 | "payload": { 185 | "field": "translate", 186 | "arguments": $utils.toJson($context.args) 187 | } 188 | } 189 | ResponseMappingTemplate: | 190 | $util.toJson($context.result) 191 | queryDetectLanguageResolver: 192 | Type: "AWS::AppSync::Resolver" 193 | Properties: 194 | ApiId: !Ref appSyncAPI 195 | TypeName: "Query" 196 | FieldName: "detectLanguage" 197 | DataSourceName: !GetAtt ChatQLAIProxyDataSource.Name 198 | RequestMappingTemplate: | 199 | { 200 | "version" : "2017-02-28", 201 | "operation": "Invoke", 202 | "payload": { 203 | "field": "comprehend-language", 204 | "arguments": $utils.toJson($context.args) 205 | } 206 | } 207 | ResponseMappingTemplate: | 208 | $util.toJson($context.result) 209 | queryDetectEntitiesResolver: 210 | Type: "AWS::AppSync::Resolver" 211 | Properties: 212 | ApiId: !Ref appSyncAPI 213 | TypeName: "Query" 214 | FieldName: "detectEntities" 215 | DataSourceName: !GetAtt ChatQLAIProxyDataSource.Name 216 | RequestMappingTemplate: | 217 | { 218 | "version" : "2017-02-28", 219 | "operation": "Invoke", 220 | "payload": { 221 | "field": "comprehend-entities", 222 | "arguments": $utils.toJson($context.args) 223 | } 224 | } 225 | ResponseMappingTemplate: | 226 | $util.toJson($context.result) 227 | queryDetectSentimentResolver: 228 | Type: "AWS::AppSync::Resolver" 229 | Properties: 230 | ApiId: !Ref appSyncAPI 231 | TypeName: "Query" 232 | FieldName: "detectSentiment" 233 | DataSourceName: !GetAtt ChatQLAIProxyDataSource.Name 234 | RequestMappingTemplate: | 235 | { 236 | "version" : "2017-02-28", 237 | "operation": "Invoke", 238 | "payload": { 239 | "field": "comprehend-sentiment", 240 | "arguments": $utils.toJson($context.args) 241 | } 242 | } 243 | ResponseMappingTemplate: | 244 | $util.toJson($context.result) 245 | -------------------------------------------------------------------------------- /backend/moviebot-lambda/Intents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/backend/moviebot-lambda/Intents.png -------------------------------------------------------------------------------- /backend/moviebot-lambda/Slots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/backend/moviebot-lambda/Slots.png -------------------------------------------------------------------------------- /backend/moviebot-lambda/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tmdb = require("tmdbv3").init("35440259b50e646a6485055c83367ccd"); 3 | 4 | // Close dialog with the customer, reporting fulfillmentState of Failed or Fulfilled ("Thanks, your pizza will arrive in 20 minutes") 5 | function close(sessionAttributes, fulfillmentState, message) { 6 | return { 7 | sessionAttributes, 8 | dialogAction: { 9 | type: "Close", 10 | fulfillmentState, 11 | message 12 | } 13 | }; 14 | } 15 | 16 | // --------------- Events ----------------------- 17 | 18 | function dispatch(intentRequest, callback) { 19 | console.log( 20 | "request received for userId=${intentRequest.userId}, intentName=${intentRequest.currentIntent.intentName}" 21 | ); 22 | const sessionAttributes = intentRequest.sessionAttributes; 23 | const slots = intentRequest.currentIntent.slots; 24 | var moviename = slots.name; 25 | var whatInfo = slots.summary; 26 | console.log(`request received for Slots=${moviename}, ${whatInfo}`); 27 | 28 | if (moviename.includes("@moviebot")) { 29 | let temp = moviename.replace("@moviebot", ""); 30 | moviename = temp; 31 | } 32 | if (whatInfo && whatInfo.includes("@moviebot")) { 33 | let temp = whatInfo.replace("@moviebot", ""); 34 | whatInfo = temp; 35 | } 36 | if (moviename.includes("moviebot")) { 37 | let temp = moviename.replace("moviebot", ""); 38 | moviename = temp; 39 | } 40 | if (whatInfo && whatInfo.includes("moviebot")) { 41 | let temp = whatInfo.replace("moviebot", ""); 42 | whatInfo = temp; 43 | } 44 | if (moviename.includes("@Moviebot")) { 45 | let temp = moviename.replace("@Moviebot", ""); 46 | moviename = temp; 47 | } 48 | if (whatInfo && whatInfo.includes("@Moviebot")) { 49 | let temp = whatInfo.replace("@Moviebot", ""); 50 | whatInfo = temp; 51 | } 52 | if (moviename.includes("Moviebot")) { 53 | let temp = moviename.replace("Moviebot", ""); 54 | moviename = temp; 55 | } 56 | if (whatInfo && whatInfo.includes("Moviebot")) { 57 | let temp = whatInfo.replace("Moviebot", ""); 58 | whatInfo = temp; 59 | } 60 | 61 | if (!moviename) { 62 | } 63 | 64 | console.log("After moviebot checks: " + moviename, whatInfo); 65 | 66 | tmdb.search.movie(`${moviename}`, function(err, res) { 67 | if (err) console.log(err); 68 | else if (res.errors) { 69 | callback( 70 | close(sessionAttributes, "Fulfilled", { 71 | contentType: "PlainText", 72 | content: 73 | "Please try something like: [@moviebot Tell me about a movie], [@moviebot Tell me about [Movie]], [@moviebot [Movie] [Plot]], [@moviebot [Movie] [Year]] or even just [@moviebot [Movie]]" 74 | }) 75 | ); 76 | } else { 77 | tmdb.movie.info(res.results[0].id, function(err, res) { 78 | if (err) { 79 | callback( 80 | close(sessionAttributes, "Fulfilled", { 81 | contentType: "PlainText", 82 | content: 83 | "Please try something like: [@moviebot Tell me about a movie], [@moviebot Tell me about [Movie]], [@moviebot [Movie] [Plot]], [@moviebot [Movie] [Year]] or even just [@moviebot [Movie]]" 84 | }) 85 | ); 86 | } 87 | var resPlot = res.overview; 88 | var resDate = res.release_date; 89 | var resLink = "https://www.imdb.com/title/" + res.imdb_id; 90 | var resPoster = "https://image.tmdb.org/t/p/w185/" + res.poster_path; 91 | console.log(res); 92 | if ( 93 | whatInfo === "Year" || 94 | whatInfo === "Release Date" || 95 | whatInfo === "release date" || 96 | whatInfo === "date" || 97 | whatInfo === "date" || 98 | whatInfo === "Release Year" || 99 | whatInfo === "release year" || 100 | whatInfo === "year" 101 | ) { 102 | callback( 103 | close(sessionAttributes, "Fulfilled", { 104 | contentType: "PlainText", 105 | content: ` ${moviename} released in: ${resDate} - IMDB Link: ${resLink} - IMDB Poster: ${resPoster}` 106 | }) 107 | ); 108 | } else if ( 109 | whatInfo === "Plot" || 110 | whatInfo === "Story" || 111 | whatInfo === "plot" || 112 | whatInfo === "story" 113 | ) { 114 | callback( 115 | close(sessionAttributes, "Fulfilled", { 116 | contentType: "PlainText", 117 | content: `Plot of ${moviename} is: ${resPlot} - IMDB Link: ${resLink} - IMDB Poster: ${resPoster} - IMDB Poster: ${resPoster}` 118 | }) 119 | ); 120 | } else 121 | callback( 122 | close(sessionAttributes, "Fulfilled", { 123 | contentType: "PlainText", 124 | content: `Movie Name: ${moviename}, Year: ${resDate}, Plot: ${resPlot} - IMDB Link: ${resLink} - IMDB Poster: ${resPoster}` 125 | }) 126 | ); 127 | }); 128 | } 129 | }); 130 | } 131 | 132 | // --------------- Main handler ----------------------- 133 | 134 | // Route the incoming request based on intent. 135 | // The JSON body of the request is provided in the event slot. 136 | exports.handler = (event, context, callback) => { 137 | try { 138 | dispatch(event, response => { 139 | console.log(response); 140 | callback(null, response); 141 | }); 142 | } catch (err) { 143 | callback(err); 144 | } 145 | }; 146 | -------------------------------------------------------------------------------- /backend/moviebot-lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moviebot-lambda", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "description": "", 11 | "dependencies": { 12 | "tmdbv3": "^0.1.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/schema.graphql: -------------------------------------------------------------------------------- 1 | type Conversation 2 | @model( 3 | mutations: { create: "createConvo" } 4 | queries: { get: "getConvo" } 5 | subscriptions: null 6 | ) { 7 | id: ID! 8 | name: String! 9 | createdAt: String 10 | # Messages in this conversation 11 | messages: [Message] @connection(name: "ConvoMsgs", sortField: "createdAt") 12 | associated: [ConvoLink] @connection(name: "associatedLinks") 13 | } 14 | 15 | type Message 16 | @searchable 17 | @auth(rules: [{ allow: owner, identityField: "sub", mutations: [create] }]) 18 | @model( 19 | mutations: { create: "createMessage" } 20 | queries: null 21 | subscriptions: null 22 | ) { 23 | id: ID! 24 | content: String 25 | createdAt: String 26 | owner: String 27 | chatbot: Boolean 28 | # Flag denoting if this message has been accepted by the server or not. 29 | isSent: Boolean 30 | file: S3Object 31 | messageConversationId: ID 32 | # The conversation this message belongs to 33 | conversation: Conversation @connection(name: "ConvoMsgs") 34 | } 35 | 36 | type S3Object { 37 | bucket: String! 38 | region: String! 39 | key: String! 40 | } 41 | 42 | type User 43 | @searchable 44 | @auth(rules: [{ allow: owner, identityField: "sub" }]) 45 | @model( 46 | mutations: { create: "registerUser" } 47 | queries: { get: "getUser" } 48 | subscriptions: null 49 | ) { 50 | id: ID! 51 | username: String! 52 | # is the user registered? 53 | registered: Boolean 54 | # the user's conversations 55 | userConversations: [ConvoLink] @connection(name: "UserLinks") 56 | } 57 | 58 | type ConvoLink 59 | @searchable 60 | @model( 61 | mutations: { create: "createConvoLink", update: "updateConvoLink" } 62 | queries: null 63 | subscriptions: null 64 | ) { 65 | id: ID! 66 | name: String 67 | status: String 68 | convoLinkUserId: ID 69 | user: User @connection(name: "UserLinks") 70 | conversation: Conversation @connection(name: "associatedLinks") 71 | } 72 | 73 | type AI { 74 | bucket: String 75 | key: String 76 | bot: String 77 | text: String 78 | language: String 79 | voice: String 80 | response: AWSJSON 81 | } 82 | 83 | type Subscription { 84 | onCreateMessage(messageConversationId: ID!): Message 85 | @aws_subscribe(mutations: ["createMessage"]) 86 | onUpdateConvoLink(convoLinkUserId: ID, status: String): ConvoLink 87 | @aws_subscribe(mutations: ["updateConvoLink"]) 88 | } 89 | 90 | type Query { 91 | detectCelebs(bucket: String, key: String): AI 92 | detectLabels(bucket: String, key: String): AI 93 | detectLanguage(text: String): AI 94 | detectEntities(language: String, text: String): AI 95 | detectSentiment(language: String, text: String): AI 96 | invokeBot(bot: String, text: String): AI 97 | dictate(bucket: String, key: String, voice: String, text: String): AI 98 | translate(language: String, text: String): AI 99 | } 100 | -------------------------------------------------------------------------------- /media/ChatQLv2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/media/ChatQLv2.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatql-react-app-reinvent", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "aws-amplify": "^2.2.6", 7 | "aws-amplify-react": "^3.1.7", 8 | "aws-appsync": "^3.0.2", 9 | "aws-appsync-react": "^3.0.2", 10 | "bootstrap": "^4.4.1", 11 | "downshift": "^5.0.5", 12 | "graphql-tag": "^2.10.3", 13 | "lodash.clonedeep": "^4.5.0", 14 | "lodash.debounce": "^4.0.8", 15 | "lodash.get": "^4.4.2", 16 | "moment": "^2.24.0", 17 | "node-sass-chokidar": "^1.4.0", 18 | "react": "^16.5.2", 19 | "react-apollo": "^3.1.3", 20 | "react-click-n-hold": "^1.0.7", 21 | "react-custom-scrollbars": "^4.2.1", 22 | "react-d3-cloud": "^0.7.0", 23 | "react-dom": "^16.5.2", 24 | "react-scripts": "2.1.1", 25 | "react-sizeme": "^2.6.12", 26 | "react-sound": "^1.2.0", 27 | "react-speech-recognition": "^2.0.4", 28 | "react-spinners": "^0.8.1", 29 | "react-tiny-popover": "^4.0.0", 30 | "reactstrap": "^8.4.1", 31 | "rxjs": "^6.5.4", 32 | "uuid": "^7.0.2" 33 | }, 34 | "scripts": { 35 | "build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/", 36 | "watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive", 37 | "start-js": "react-scripts start", 38 | "build-js": "react-scripts build", 39 | "start": "npm-run-all -p watch-css start-js", 40 | "build": "npm-run-all build-css build-js", 41 | "test": "react-scripts test --env=jsdom", 42 | "eject": "react-scripts eject" 43 | }, 44 | "browserslist": [ 45 | ">0.2%", 46 | "not dead", 47 | "not ie <= 11", 48 | "not op_mini all" 49 | ], 50 | "devDependencies": { 51 | "babel-core": "^6.26.3", 52 | "babel-eslint": "^10.1.0", 53 | "babel-runtime": "^6.26.0", 54 | "eslint-config-prettier": "^6.10.1", 55 | "eslint-config-standard": "^14.1.1", 56 | "eslint-plugin-import": "^2.20.1", 57 | "eslint-plugin-node": "^11.0.0", 58 | "eslint-plugin-promise": "^4.2.1", 59 | "eslint-plugin-standard": "^4.0.1", 60 | "npm-run-all": "^4.1.5", 61 | "prettier-eslint": "^9.0.1", 62 | "standard": "^14.3.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicon.ico -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/public/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /public/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChatQL", 3 | "short_name": "ChatQL", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ff9900", 17 | "background_color": "#ff9900", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 32 | ChatQL - The React Way 33 | 34 | 35 | 36 | 39 |
40 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChatQL", 3 | "short_name": "ChatQL", 4 | "start_url": "/", 5 | "theme_color": "#ff9900", 6 | "background_color": "#ff9900", 7 | "display": "standalone", 8 | "orientation": "any", 9 | "icons": [ 10 | { 11 | "src": "favicons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @keyframes App-logo-spin { 2 | from { 3 | transform: rotate(0deg); } 4 | to { 5 | transform: rotate(360deg); } } 6 | 7 | .drawer { 8 | -webkit-animation: slide-in 0.2s ease-in; 9 | -moz-animation: slide-in 0.2s ease-in; } 10 | 11 | @-webkit-keyframes slide-in { 12 | 0% { 13 | opacity: 0; 14 | -webkit-transform: translateX(-100%); } 15 | 100% { 16 | opacity: 1; 17 | -webkit-transform: translateX(0); } } 18 | 19 | @-moz-keyframes slide-in { 20 | 0% { 21 | opacity: 0; 22 | -moz-transform: translateX(-100%); } 23 | 100% { 24 | opacity: 1; 25 | -moz-transform: translateX(0); } } 26 | 27 | .viewer .navbar-toggler { 28 | display: none; } 29 | 30 | .drawer .navbar-toggler { 31 | display: none; } 32 | 33 | @media (max-width: 768px) { 34 | .drawer { 35 | display: none; } 36 | .viewer .navbar-toggler { 37 | display: inherit; } 38 | .switchview .drawer { 39 | display: block !important; 40 | width: 100% !important; 41 | max-width: 100% !important; 42 | flex: 0 0 100%; } 43 | .switchview .drawer .navbar-toggler { 44 | display: inherit; } 45 | .switchview .viewer { 46 | display: none !important; } } 47 | 48 | .pane { 49 | height: calc(100% - 110px); 50 | overflow: hidden; 51 | overflow-y: auto; } 52 | 53 | .sidelistx, 54 | .scrollArea { 55 | height: calc(100% - 54px); 56 | overflow: hidden; 57 | overflow-y: auto; } 58 | 59 | .downshift-inner { 60 | height: calc(100% - 56px); } 61 | 62 | .messageList { 63 | padding: 1em; } 64 | 65 | .loader { 66 | color: purple; 67 | width: 100%; } 68 | 69 | .chatMsg { 70 | min-width: 25%; 71 | max-width: 75%; } 72 | 73 | .list-group-flush .list-group-item:first-child { 74 | border-top: 0; } 75 | 76 | .list-group-flush h3 { 77 | margin-bottom: 0; } 78 | 79 | .file-input-holder { 80 | width: 42px; 81 | position: relative; 82 | display: flex !important; } 83 | .file-input-holder input[type='file'] { 84 | width: 42px; 85 | height: 38px; 86 | z-index: 2; 87 | margin: 0; 88 | opacity: 0; 89 | overflow: hidden; 90 | position: relative; } 91 | .file-input-holder label { 92 | position: absolute; 93 | top: 0; 94 | left: 0; } 95 | 96 | .file-placeholder { 97 | height: 200px; } 98 | 99 | .msg-image { 100 | max-width: 100%; 101 | max-height: 300px; } 102 | 103 | @media (max-width: 768px) { 104 | .msg-image { 105 | max-height: 200px !important; } } 106 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import './App.css' 4 | import Amplify, { Auth } from 'aws-amplify' 5 | import awsmobile from './aws-exports' 6 | import { withAuthenticator } from 'aws-amplify-react' 7 | import AWSAppSyncClient from 'aws-appsync' 8 | import { Rehydrated } from 'aws-appsync-react' 9 | 10 | import { ApolloProvider } from 'react-apollo' 11 | import { ChatAppWithData } from './components/chatapp' 12 | 13 | Amplify.configure(awsmobile) 14 | 15 | const client = new AWSAppSyncClient({ 16 | url: awsmobile.aws_appsync_graphqlEndpoint, 17 | region: awsmobile.aws_appsync_region, 18 | auth: { 19 | type: awsmobile.aws_appsync_authenticationType, 20 | jwtToken: async () => 21 | (await Auth.currentSession()).getIdToken().getJwtToken() 22 | }, 23 | complexObjectsCredentials: () => Auth.currentCredentials() 24 | }) 25 | 26 | class App extends Component { 27 | state = { session: null } 28 | 29 | async componentDidMount() { 30 | const session = await Auth.currentSession() 31 | this.setState({ session }) 32 | } 33 | 34 | userInfo = () => { 35 | const session = this.state.session 36 | if (!session) { 37 | return {} 38 | } 39 | const payload = session.idToken.payload 40 | return { name: payload['cognito:username'], id: payload['sub'] } 41 | } 42 | 43 | render() { 44 | return 45 | } 46 | } 47 | 48 | const WithProvider = () => ( 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | 56 | export default withAuthenticator(WithProvider) 57 | -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | @keyframes App-logo-spin { 2 | from { 3 | transform: rotate(0deg); 4 | } 5 | to { 6 | transform: rotate(360deg); 7 | } 8 | } 9 | 10 | .drawer { 11 | -webkit-animation: slide-in 0.2s ease-in; 12 | -moz-animation: slide-in 0.2s ease-in; 13 | } 14 | 15 | @-webkit-keyframes slide-in { 16 | 0% { 17 | opacity: 0; 18 | -webkit-transform: translateX(-100%); 19 | } 20 | 100% { 21 | opacity: 1; 22 | -webkit-transform: translateX(0); 23 | } 24 | } 25 | @-moz-keyframes slide-in { 26 | 0% { 27 | opacity: 0; 28 | -moz-transform: translateX(-100%); 29 | } 30 | 100% { 31 | opacity: 1; 32 | -moz-transform: translateX(0); 33 | } 34 | } 35 | 36 | .viewer { 37 | .navbar-toggler { 38 | display: none; 39 | } 40 | } 41 | 42 | .drawer { 43 | .navbar-toggler { 44 | display: none; 45 | } 46 | } 47 | 48 | @media (max-width: 768px) { 49 | .drawer { 50 | display: none; 51 | } 52 | 53 | .viewer { 54 | .navbar-toggler { 55 | display: inherit; 56 | } 57 | } 58 | 59 | .switchview { 60 | .drawer { 61 | display: block !important; 62 | width: 100% !important; 63 | max-width: 100% !important; 64 | flex: 0 0 100%; 65 | .navbar-toggler { 66 | display: inherit; 67 | } 68 | } 69 | .viewer { 70 | display: none !important; 71 | } 72 | } 73 | } 74 | 75 | .pane { 76 | height: calc(100% - 110px); 77 | overflow: hidden; 78 | overflow-y: auto; 79 | } 80 | 81 | .sidelistx, 82 | .scrollArea { 83 | height: calc(100% - 54px); 84 | overflow: hidden; 85 | overflow-y: auto; 86 | } 87 | .downshift-inner { 88 | height: calc(100% - 56px); 89 | } 90 | 91 | .messageList { 92 | padding: 1em; 93 | } 94 | 95 | .loader { 96 | color: purple; 97 | width: 100%; 98 | } 99 | 100 | .chatMsg { 101 | min-width: 25%; 102 | max-width: 75%; 103 | } 104 | 105 | .list-group-flush .list-group-item:first-child { 106 | border-top: 0; 107 | } 108 | 109 | .list-group-flush h3 { 110 | margin-bottom: 0; 111 | } 112 | 113 | .file-input-holder { 114 | width: 42px; 115 | position: relative; 116 | display: flex !important; 117 | 118 | input[type='file'] { 119 | width: 42px; 120 | height: 38px; 121 | z-index: 2; 122 | margin: 0; 123 | opacity: 0; 124 | overflow: hidden; 125 | position: relative; 126 | } 127 | 128 | label { 129 | position: absolute; 130 | top: 0; 131 | left: 0; 132 | } 133 | } 134 | 135 | .file-placeholder { 136 | height: 200px; 137 | } 138 | 139 | .msg-image { 140 | max-width: 100%; 141 | max-height: 300px; 142 | } 143 | 144 | @media (max-width: 768px) { 145 | .msg-image { 146 | max-height: 200px !important; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/AI/Bot.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { graphql, ApolloConsumer } from 'react-apollo' 4 | import invokeBot from '../../graphql/AI/invokeBot' 5 | import { createMessage } from '../../graphql/mutations' 6 | import lexlogo from '../../images/lexlogo.png' 7 | import uuid from 'uuid/v4' 8 | 9 | const QueryMap = [ 10 | { name: 'Developers', slot: 'development' }, 11 | { name: 'Movies', slot: 'movies' }, 12 | { name: 'Science', slot: 'science' }, 13 | { name: 'Music', slot: 'music' }, 14 | { name: 'Food', slot: 'food' } 15 | ] 16 | async function lex(client, botName, msg, slot, update) { 17 | const lex = await client.query({ 18 | query: invokeBot, 19 | fetchPolicy: 'network-only', 20 | variables: { 21 | bot: botName, 22 | text: `Give me a Chuck Norris ${slot} fact` 23 | } 24 | }) 25 | let botResponse = JSON.parse(lex.data.invokeBot.response) 26 | update({ botResponse }) 27 | console.log('Sending Lex response mutation to backend...') 28 | const botMessage = { 29 | input: { 30 | id: uuid(), 31 | content: `[${botName}] ${botResponse}`, 32 | messageConversationId: msg.messageConversationId, 33 | chatbot: true, 34 | isSent: true, 35 | file: null 36 | } 37 | } 38 | await client.mutate({ 39 | mutation: createMessage, 40 | variables: botMessage, 41 | optimisticResponse: { 42 | createMessage: { 43 | __typename: 'Message', 44 | ...botMessage.input, 45 | owner: msg.owner, 46 | isSent: false, 47 | conversation: { 48 | __typename: 'Conversation', 49 | id: msg.messageConversationId, 50 | name: 'n/a', 51 | createdAt: 'n/a' 52 | }, 53 | createdAt: new Date().toISOString() 54 | } 55 | } 56 | }) 57 | } 58 | 59 | function Bot(props) { 60 | return ( 61 | 62 | {client => ( 63 | 64 | {props.botName === 'ChuckBot' ? ( 65 | 66 | ) : ( 67 | 68 | )} 69 | 70 | )} 71 | 72 | ) 73 | } 74 | 75 | function ChuckBot({ msg, update, botName, client }) { 76 | return ( 77 |
78 |
79 |
80 |
81 | 82 | Select the Chuck Norris fact: 83 | 84 |
85 |
86 |
87 |
88 |
89 |

90 | 91 | Powered by: 92 | 93 |

94 | Amazon Lex 101 | 102 | & 103 | 104 | Powered by chucknorris.io 111 |
112 | {QueryMap.map((item, i) => ( 113 | 121 | ))} 122 |
123 |
124 |
125 | ) 126 | } 127 | 128 | class MovieBot extends React.Component { 129 | state = { created: false, response: null } 130 | componentDidMount() { 131 | const { data: { invokeBot: { response } = {} } = {} } = this.props 132 | if (response) { 133 | this.setState({ response }, this.createMessage) 134 | } 135 | } 136 | 137 | async componentDidUpdate(prevProps, prevState, snapshot) { 138 | const { 139 | data: { invokeBot: { response: oldResponse = null } = {} } = {} 140 | } = prevProps 141 | const { data: { invokeBot: { response } = {} } = {} } = this.props 142 | 143 | console.log(oldResponse, response) 144 | if (!oldResponse && response && !this.state.response) { 145 | this.setState({ response }, this.createMessage) 146 | } 147 | } 148 | 149 | async createMessage() { 150 | const text = JSON.parse(this.state.response) 151 | console.log('Movie Lex response:', text) 152 | 153 | const botMessage = { 154 | input: { 155 | id: uuid(), 156 | content: `[${this.props.botName}] ${text}`, 157 | messageConversationId: this.props.msg.messageConversationId, 158 | chatbot: true, 159 | isSent: true, 160 | file: null 161 | } 162 | } 163 | 164 | await this.props.client.mutate({ 165 | mutation: createMessage, 166 | variables: botMessage, 167 | optimisticResponse: { 168 | createMessage: { 169 | __typename: 'Message', 170 | ...botMessage.input, 171 | owner: this.props.msg.owner, 172 | isSent: false, 173 | conversation: { 174 | __typename: 'Conversation', 175 | id: this.props.msg.messageConversationId, 176 | name: 'n/a', 177 | createdAt: 'n/a' 178 | }, 179 | createdAt: new Date().toISOString() 180 | } 181 | } 182 | }) 183 | } 184 | 185 | render() { 186 | return null 187 | } 188 | } 189 | 190 | Bot.propTypes = { 191 | data: PropTypes.object, 192 | msg: PropTypes.object.isRequired, 193 | botName: PropTypes.string.isRequired, 194 | update: PropTypes.func.isRequired 195 | } 196 | ChuckBot.propTypes = { 197 | msg: PropTypes.object.isRequired, 198 | client: PropTypes.object.isRequired, 199 | botName: PropTypes.string.isRequired, 200 | update: PropTypes.func.isRequired 201 | } 202 | MovieBot.propTypes = { 203 | data: PropTypes.object, 204 | msg: PropTypes.object.isRequired, 205 | client: PropTypes.object.isRequired, 206 | botName: PropTypes.string.isRequired, 207 | update: PropTypes.func.isRequired 208 | } 209 | export default graphql(invokeBot, { 210 | skip: props => !props.msg.content, 211 | options: props => ({ 212 | fetchPolicy: 'network-only', 213 | variables: { 214 | bot: props.botName, 215 | text: props.msg.content 216 | } 217 | }) 218 | })(Bot) 219 | -------------------------------------------------------------------------------- /src/components/AI/DetectCelebs.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'react-apollo' 3 | import rekognition from '../../images/rekognition.png' 4 | import detectCelebs from '../../graphql/AI/detectCelebs' 5 | 6 | function Celebs({ data: { loading, error, detectCelebs } }) { 7 | if (loading) { 8 | return ( 9 |
10 |
11 | 12 | Detecting Celebrities... 13 | 14 |
15 |
16 | ) 17 | } 18 | 19 | if (error) { 20 | const err = JSON.stringify(error.message) 21 | return ( 22 |
23 | 24 | {err} 25 | 26 |
27 | ) 28 | } 29 | 30 | const response = JSON.parse(detectCelebs.response) 31 | return ( 32 |
33 | {response.length && ( 34 |
35 |
36 |
37 | 38 | 39 | 40 | Amazon Rekognition 48 | Celebs 49 | 50 | 51 | 52 |
53 | 54 | 55 | 56 | 57 | 60 | 63 | 64 | 65 | 66 | {response.map((item, i) => ( 67 | 68 | 69 | 74 | 87 | 88 | ))} 89 | 90 |
# 58 | Name 59 | 61 | Confidence 62 |
{i + 1} 70 | 71 | {item.Name} 72 | 73 | 75 | 79 | 80 | {( 81 | Math.round(item.MatchConfidence * 10000) / 10000 82 | ).toFixed(2)} 83 | % 84 | 85 | 86 |
91 |
92 |
93 | )} 94 |
95 | ) 96 | } 97 | export default graphql(detectCelebs, { 98 | skip: props => !props.path, 99 | options: props => ({ 100 | variables: { 101 | bucket: props.bucket, 102 | key: props.path 103 | } 104 | }) 105 | })(Celebs) 106 | -------------------------------------------------------------------------------- /src/components/AI/DetectEntities.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { graphql } from 'react-apollo' 4 | import comprehend from '../../images/comprehend.png' 5 | import detectEntities from '../../graphql/AI/detectEntities' 6 | 7 | const score = val => Number.parseFloat(val * 100).toFixed(2) + '%' 8 | 9 | function DetectEntities({ data: { loading, error, detectEntities } }) { 10 | if (loading) { 11 | return ( 12 |
13 |
14 | 15 | Detecting Entities... 16 | 17 |
18 |
19 | ) 20 | } else if (error) { 21 | const err = JSON.stringify(error.message) 22 | return ( 23 |
24 | 25 | {err} 26 | 27 |
28 | ) 29 | } 30 | 31 | const response = JSON.parse(detectEntities.response) 32 | 33 | if (!response.length) { 34 | return null 35 | } 36 | 37 | return ( 38 |
39 |
40 |
41 | 42 | 43 | 44 | Amazon Comprehend 52 | Entities 53 | 54 | 55 | 56 |
57 | 58 | 59 | 60 | 63 | 66 | 69 | 70 | 71 | 72 | {response.map((item, i) => ( 73 | 74 | 79 | 84 | 89 | 90 | ))} 91 | 92 |
61 | Text 62 | 64 | Type 65 | 67 | Score 68 |
75 | 76 | {item.Text} 77 | 78 | 80 | 81 | {item.Type} 82 | 83 | 85 | 86 | {score(item.Score)} 87 | 88 |
93 |
94 |
95 | ) 96 | } 97 | DetectEntities.propTypes = { 98 | data: PropTypes.object.isRequired 99 | } 100 | export default graphql(detectEntities, { 101 | skip: props => !props.text, 102 | options: props => ({ 103 | variables: { 104 | language: props.language, 105 | text: props.text 106 | } 107 | }) 108 | })(DetectEntities) 109 | -------------------------------------------------------------------------------- /src/components/AI/DetectLabels.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'react-apollo' 3 | import rekognition from '../../images/rekognition.png' 4 | import detectLabels from '../../graphql/AI/detectLabels' 5 | 6 | function Labels({ data: { loading, error, detectLabels } }) { 7 | if (loading) { 8 | return ( 9 |
10 |
11 | 12 | Detecting Labels... 13 | 14 |
15 |
16 | ) 17 | } 18 | 19 | if (error) { 20 | const err = JSON.stringify(error.message) 21 | return ( 22 |
23 | 24 | {err} 25 | 26 |
27 | ) 28 | } 29 | 30 | const response = JSON.parse(detectLabels.response) 31 | return ( 32 |
33 |
34 |
35 |
36 | 37 | 38 | 39 | Amazon Rekognition 47 | Labels 48 | 49 | 50 | 51 |
52 | 53 | 54 | 55 | 56 | 59 | 62 | 63 | 64 | 65 | {response.map((item, i) => ( 66 | 67 | 68 | 73 | 83 | 84 | ))} 85 | 86 |
# 57 | Label 58 | 60 | Confidence 61 |
{i + 1} 69 | 70 | {item.Name} 71 | 72 | 74 | 75 | 76 | {(Math.round(item.Confidence * 10000) / 10000).toFixed( 77 | 2 78 | )} 79 | % 80 | 81 | 82 |
87 |
88 |
89 |
90 | ) 91 | } 92 | export default graphql(detectLabels, { 93 | skip: props => !props.path, 94 | options: props => ({ 95 | variables: { 96 | bucket: props.bucket, 97 | key: props.path 98 | } 99 | }) 100 | })(Labels) 101 | -------------------------------------------------------------------------------- /src/components/AI/DetectLanguage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'react-apollo' 3 | import detectLanguage from '../../graphql/AI/detectLanguage' 4 | 5 | class DetectLanguage extends React.Component { 6 | sendLanguage = language => { 7 | this.props.getDetectedLanguage(language) 8 | } 9 | componentDidMount() { 10 | const { 11 | data: { loading, error, detectLanguage } 12 | } = this.props 13 | if (loading) { 14 | console.log('Detecting Language... ' + loading) 15 | } else if (error) { 16 | console.log(error) 17 | } else { 18 | const response = JSON.parse(detectLanguage.response) 19 | const language = response[0].LanguageCode 20 | this.sendLanguage(language) 21 | } 22 | } 23 | render() { 24 | //const { data } = this.props; 25 | //console.log(JSON.stringify(data)); 26 | return null 27 | } 28 | } 29 | export default graphql(detectLanguage, { 30 | skip: props => !props.text, 31 | options: props => ({ 32 | variables: { 33 | text: props.text 34 | }, 35 | fetchPolicy: 'cache-and-network' 36 | }) 37 | })(DetectLanguage) 38 | -------------------------------------------------------------------------------- /src/components/AI/DetectSentiment.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { graphql } from 'react-apollo' 4 | import comprehend from '../../images/comprehend.png' 5 | import detectSentiment from '../../graphql/AI/detectSentiment' 6 | 7 | const score = val => Number.parseFloat(val * 100).toFixed(2) + '%' 8 | const getEmoji = Sentiment => { 9 | let emoji = 10 | if (Sentiment === 'POSITIVE') { 11 | emoji = 12 | } else if (Sentiment === 'NEGATIVE') { 13 | emoji = 14 | } else if (Sentiment === 'NEUTRAL') { 15 | emoji = 16 | } else if (Sentiment === 'MIXED') { 17 | emoji = 18 | } 19 | return emoji 20 | } 21 | function DetectSentiment({ data: { loading, error, detectSentiment } }) { 22 | if (loading) { 23 | return ( 24 |
25 |
26 | 27 | Performing Analysis... 28 | 29 |
30 |
31 | ) 32 | } else if (error) { 33 | const err = JSON.stringify(error.message) 34 | return ( 35 |
36 | 37 | {err} 38 | 39 |
40 | ) 41 | } 42 | 43 | const response = JSON.parse(detectSentiment.response) 44 | const { Sentiment, SentimentScore: scores } = response 45 | 46 | return ( 47 |
48 |
49 |
50 | 51 | 52 | 53 | 58 | 59 | 60 |
54 |

55 | {getEmoji(Sentiment)} 56 |

57 |
61 |
62 | 63 | 64 | 65 | Amazon Comprehend 73 | Scores 74 | 75 | 76 | 77 |
78 | 79 | 80 | {['Positive', 'Negative', 'Neutral', 'Mixed'].map(s => ( 81 | 82 | 85 | 90 | 91 | ))} 92 | 93 |
83 | {s} 84 | 86 | 87 | {score(scores[s])} 88 | 89 |
94 |
95 |
96 |
97 | ) 98 | } 99 | 100 | DetectSentiment.propTypes = { 101 | data: PropTypes.object.isRequired 102 | } 103 | export default graphql(detectSentiment, { 104 | skip: props => !props.text, 105 | options: props => ({ 106 | variables: { 107 | language: props.language, 108 | text: props.text 109 | } 110 | }) 111 | })(DetectSentiment) 112 | -------------------------------------------------------------------------------- /src/components/AI/Dictate.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'react-apollo' 3 | import polly from '../../images/polly.png' 4 | import dictate from '../../graphql/AI/dictate' 5 | import Sound from 'react-sound' 6 | 7 | function Dictate({ data: { loading, error, dictate }, completed }) { 8 | if (loading) { 9 | return ( 10 |
11 |
12 | 13 | Retrieving Voice / Accent / Language combo... 14 | 15 |
16 |
17 | ) 18 | } 19 | 20 | if (error) { 21 | const err = JSON.stringify(error.message) 22 | return ( 23 |
24 | 25 | {err} 26 | 27 |
28 | ) 29 | } 30 | 31 | const response = JSON.parse(dictate.response) 32 | console.log('Dictate >', response) 33 | return ( 34 |
35 |
36 |
37 | 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | Amazon Polly 59 |
60 |
61 | ) 62 | } 63 | export default graphql(dictate, { 64 | options: props => ({ 65 | skip: props => !props.text, 66 | variables: { 67 | bucket: props.bucket, 68 | key: props.path, 69 | voice: props.voice, 70 | text: props.text 71 | } 72 | }) 73 | })(Dictate) 74 | -------------------------------------------------------------------------------- /src/components/AI/InvokeBot.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class InvokeBot extends React.Component { 4 | render() { 5 | let imdbLink 6 | let imdbPoster 7 | const { text } = this.props 8 | // console.log(this.props) 9 | const expression = /(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/gi 10 | const matches = text.match(expression) 11 | const withoutLinks = text.split('- IMDB') 12 | // console.log(withoutLinks[0]) 13 | const parts = withoutLinks[0].match( 14 | /.*Movie Name: (.*), Year: ([\d-]*), Plot: (.*)/ 15 | ) 16 | const title = parts ? ( 17 | `${parts[1]} (${parts[2].split('-')[0]})` 18 | ) : ( 19 |
20 | 21 |
22 | ) 23 | const description = parts ? parts[3] : text 24 | if (matches) { 25 | imdbLink = matches[0] 26 | imdbPoster = matches[1] 27 | } 28 | return ( 29 |
30 |
31 | {imdbPoster && ( 32 | Poster 38 | )} 39 |
40 |
{title}
41 |
42 |

{description}

43 | {imdbLink && ( 44 | 49 | View on IMDB 50 | 51 | )} 52 |
53 |
54 |
55 |
56 | ) 57 | } 58 | } 59 | export default InvokeBot 60 | -------------------------------------------------------------------------------- /src/components/AI/MenuDropDown.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { 4 | Dropdown, 5 | DropdownToggle, 6 | DropdownMenu, 7 | DropdownItem 8 | } from 'reactstrap' 9 | import { graphql } from 'react-apollo' 10 | import detectLanguage from '../../graphql/AI/detectLanguage' 11 | const langMap = [ 12 | { code: 'en', lang: 'English' }, 13 | { code: 'zh', lang: 'Chinese' }, 14 | { code: 'fr', lang: 'French' }, 15 | { code: 'pt', lang: 'Portuguese' }, 16 | { code: 'es', lang: 'Spanish' } 17 | ] 18 | const BOTS = { 19 | CHUCKBOT: 'ChuckBot', 20 | MOVIEBOT: 'MovieBot' 21 | } 22 | 23 | function readResponse(props) { 24 | const { data: { detectLanguage } = {} } = props 25 | return detectLanguage ? detectLanguage.response : undefined 26 | } 27 | 28 | class AIMenu extends React.Component { 29 | componentDidUpdate(prevProps, prevState, snapshot) { 30 | const oldResponse = readResponse(prevProps) 31 | const response = readResponse(this.props) 32 | 33 | if (!oldResponse && response) { 34 | const code = JSON.parse(response)[0].LanguageCode 35 | console.log(`Detected Language from Direct Query: ${code}`) 36 | this.props.setLanguageCode(code) 37 | } 38 | } 39 | 40 | render() { 41 | const { 42 | msg, 43 | dropdownOpen, 44 | toggleDropDown, 45 | dictate, 46 | doBot, 47 | comprehend, 48 | setTranslation 49 | } = this.props 50 | const response = readResponse(this.props) 51 | const code = response ? JSON.parse(response)[0].LanguageCode : null 52 | // console.log(JSON.stringify(this.props, null, 2), response, code) 53 | const hasText = msg.content && msg.content.trim().length 54 | return ( 55 | 56 | 60 | 61 | 62 | 63 | Listen 64 | 65 | 66 | Text to Speech 67 | 68 | Bots 69 | doBot({ bot: BOTS.CHUCKBOT })} 72 | className="small" 73 | > 74 | 75 | ChuckBot 76 | 77 | doBot({ bot: BOTS.MOVIEBOT })} 80 | className="small" 81 | > 82 | 83 | MovieBot 84 | 85 | {hasText && ( 86 | 87 | Analyze 88 | 89 | 90 | Sentiment 91 | 92 | Translate 93 | {langMap.map(l => ( 94 | setTranslation({ selectedLanguage: l.code })} 99 | className="small" 100 | > 101 | 102 | {l.lang} 103 | 104 | ))} 105 | 106 | )} 107 | 108 | 109 | ) 110 | } 111 | } 112 | 113 | AIMenu.propTypes = { 114 | msg: PropTypes.object.isRequired, 115 | dropdownOpen: PropTypes.bool.isRequired, 116 | toggleDropDown: PropTypes.func.isRequired, 117 | setLanguageCode: PropTypes.func.isRequired, 118 | setTranslation: PropTypes.func.isRequired, 119 | comprehend: PropTypes.func.isRequired, 120 | doBot: PropTypes.func.isRequired, 121 | dictate: PropTypes.func.isRequired, 122 | data: PropTypes.object 123 | } 124 | 125 | const AIMenuWithData = graphql(detectLanguage, { 126 | skip: props => !props.msg || !props.dropdownOpen, 127 | options: props => ({ 128 | variables: { 129 | text: props.msg.content 130 | } 131 | }) 132 | })(AIMenu) 133 | 134 | export default AIMenuWithData 135 | -------------------------------------------------------------------------------- /src/components/AI/Translate.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'react-apollo' 3 | import PropTypes from 'prop-types' 4 | import translate from '../../graphql/AI/translate' 5 | 6 | class Translate extends React.Component { 7 | componentDidMount() { 8 | const { data: { translate: { response } = {} } = {} } = this.props 9 | if (response) { 10 | const translated = JSON.parse(response).TranslatedText 11 | this.props.applyState({ translated }) 12 | } 13 | } 14 | 15 | componentDidUpdate(prevProps, prevState, snapshot) { 16 | const { 17 | data: { translate: { response: oldResponse = null } = {} } = {} 18 | } = prevProps 19 | const { data: { translate: { response } = {} } = {} } = this.props 20 | 21 | if (!oldResponse && response) { 22 | const translated = JSON.parse(response).TranslatedText 23 | this.props.applyState({ translated }) 24 | } 25 | } 26 | 27 | render() { 28 | const { 29 | data: { loading, error, translate } 30 | } = this.props 31 | if (loading) { 32 | console.log('Translating...') 33 | return ( 34 |
35 | 36 | Translating... 37 | 38 |
39 | ) 40 | } else if (error) { 41 | const err = JSON.stringify(error.message) 42 | return ( 43 |
44 | 45 | English is the only supported target language for the detected 46 | source language. 47 | 48 |
49 | {err} 50 |
51 | ) 52 | } 53 | const response = JSON.parse(translate.response) 54 | const translated = response.TranslatedText 55 | return {translated} 56 | } 57 | } 58 | Translate.propTypes = { 59 | data: PropTypes.object, 60 | applyState: PropTypes.func.isRequired 61 | } 62 | export default graphql(translate, { 63 | skip: props => !props.language || !props.text || !props.text.trim().length, 64 | options: props => ({ 65 | variables: { 66 | language: props.language, 67 | text: props.text.trim() 68 | } 69 | }) 70 | })(Translate) 71 | -------------------------------------------------------------------------------- /src/components/AI/TranslateCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import translate from '../../images/translate.png' 4 | import Translate from './Translate' 5 | 6 | class TranslateCard extends React.Component { 7 | render() { 8 | return ( 9 |
10 |
11 |
12 |
13 | 14 | Translation: 15 | 16 | 17 | 25 | 26 |
27 |
28 |
29 |
30 | Amazon Translate 36 |
37 |
38 | 39 | 46 | 47 |
48 |
49 |
50 | 51 | 56 | 57 |
58 |
59 | ) 60 | } 61 | } 62 | 63 | TranslateCard.propTypes = { 64 | closeTranslateCard: PropTypes.func.isRequired, 65 | dictateTranslated: PropTypes.func.isRequired, 66 | selectedLanguage: PropTypes.string.isRequired, 67 | applyState: PropTypes.func.isRequired, 68 | text: PropTypes.string.isRequired 69 | } 70 | 71 | export default TranslateCard 72 | -------------------------------------------------------------------------------- /src/components/AudioCapture.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import SpeechRecognition from "react-speech-recognition"; 3 | import ClickNHold from "react-click-n-hold"; 4 | 5 | const options = { 6 | autoStart: false 7 | }; 8 | 9 | class AudioCapture extends Component { 10 | state = { 11 | recording: false 12 | }; 13 | 14 | clickNHold() { 15 | console.log("Recording..."); 16 | this.setState({ recording: true }); 17 | } 18 | 19 | onRelease = finalTranscript => { 20 | this.sendAudio(finalTranscript); 21 | }; 22 | 23 | sendAudio = audio => { 24 | this.props.getAudioCapture(audio); 25 | }; 26 | 27 | render() { 28 | const { 29 | startListening, 30 | stopListening, 31 | resetTranscript, 32 | finalTranscript, 33 | browserSupportsSpeechRecognition 34 | } = this.props; 35 | if (!browserSupportsSpeechRecognition) { 36 | return ( 37 | 44 | ); 45 | } 46 | console.log(finalTranscript); 47 | 48 | return ( 49 |
50 | this.clickNHold(finalTranscript)} 54 | onEnd={e => { 55 | this.onRelease(finalTranscript); 56 | this.setState({ recording: false }, () => { 57 | resetTranscript(); 58 | stopListening(); 59 | }); 60 | }} 61 | > 62 | {this.state.recording ? ( 63 | 70 | ) : ( 71 | 78 | )} 79 | 80 |
81 | ); 82 | } 83 | } 84 | 85 | export default SpeechRecognition(options)(AudioCapture); 86 | -------------------------------------------------------------------------------- /src/components/ConversationBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import appsync from '../images/appsync.png' 4 | 5 | const ConversationBar = ({ conversation, name, switchView }) => { 6 | const title = 'ChatQL' + (name ? ` > ${name}` : '') 7 | return ( 8 |
9 | 29 |
30 | ) 31 | } 32 | ConversationBar.propTypes = { 33 | conversation: PropTypes.object, 34 | name: PropTypes.string, 35 | switchView: PropTypes.func.isRequired 36 | } 37 | 38 | export default ConversationBar 39 | -------------------------------------------------------------------------------- /src/components/ConvoSideList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import _get from "lodash.get"; 4 | 5 | const SideList = ({ 6 | getMenuProps, 7 | getItemProps, 8 | selectedItem, 9 | conversations 10 | }) => { 11 | const selItemId = selectedItem ? selectedItem.id : null; 12 | const items = _get(conversations, "items", []); 13 | return ( 14 |
15 |
16 | {items.length ? ( 17 | items.map((item, index) => ( 18 | 32 | )) 33 | ) : ( 34 |
35 | You have no conversations. Find a user to get started. 36 |
37 | )} 38 |
39 |
40 | ); 41 | }; 42 | SideList.propTypes = { 43 | getMenuProps: PropTypes.func.isRequired, 44 | getItemProps: PropTypes.func.isRequired, 45 | selectedItem: PropTypes.object, 46 | conversations: PropTypes.object 47 | }; 48 | 49 | export default SideList; 50 | -------------------------------------------------------------------------------- /src/components/InputBar.js: -------------------------------------------------------------------------------- 1 | /* global FileReader */ 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | import { createMessage } from '../graphql/mutations' 5 | import { getConvo } from '../graphql/queries' 6 | import { graphql } from 'react-apollo' 7 | import uuid from 'uuid/v4' 8 | import { Popover, PopoverHeader, PopoverBody } from 'reactstrap' 9 | 10 | import { Auth } from 'aws-amplify' 11 | import awsmobile from '../aws-exports' 12 | 13 | const EMPTY_FILE = { 14 | bucket: null, 15 | region: null, 16 | key: null, 17 | __typename: 'S3Object' 18 | } 19 | const VISIBILITY = 'protected' 20 | 21 | async function getFile(selectedFile) { 22 | if (!selectedFile) { 23 | return null 24 | } 25 | 26 | const bucket = awsmobile.aws_user_files_s3_bucket 27 | const region = awsmobile.aws_user_files_s3_bucket_region 28 | const { name: fileName, type: mimeType } = selectedFile 29 | const [, , , extension] = /([^.]+)(\.(\w+))?$/.exec(fileName) 30 | 31 | const { identityId } = await Auth.currentCredentials() 32 | const key = `${VISIBILITY}/${identityId}/${uuid()}${extension && 33 | '.'}${extension}` 34 | 35 | console.log(fileName, mimeType, extension, key) 36 | 37 | const file = { 38 | bucket, 39 | key, 40 | region, 41 | mimeType, 42 | localUri: selectedFile 43 | } 44 | 45 | return file 46 | } 47 | 48 | const doCreateMessage = ( 49 | mutation, 50 | content, 51 | file, 52 | convoId, 53 | userId 54 | ) => async () => { 55 | let contentTrim = content.trim() 56 | if (!contentTrim.length && file) { 57 | contentTrim = ' ' 58 | } 59 | 60 | const variables = { 61 | input: { 62 | id: uuid(), 63 | content: contentTrim, 64 | messageConversationId: convoId, 65 | isSent: true, 66 | chatbot: false, 67 | ...(file ? { file } : {}) 68 | } 69 | } 70 | 71 | mutation({ 72 | variables, 73 | optimisticResponse: { 74 | createMessage: { 75 | __typename: 'Message', 76 | ...variables.input, 77 | ...(file ? { file: EMPTY_FILE } : { file: null }), 78 | owner: userId, 79 | isSent: false, 80 | conversation: { 81 | __typename: 'Conversation', 82 | id: convoId, 83 | name: 'n/a', 84 | createdAt: 'n/a' 85 | }, 86 | createdAt: new Date().toISOString() 87 | } 88 | }, 89 | update: (proxy, { data: { createMessage: newMsg } }) => { 90 | const QUERY = { 91 | query: getConvo, 92 | variables: { id: convoId } 93 | } 94 | const prev = proxy.readQuery(QUERY) 95 | // console.log('view prev', JSON.stringify(prev, null, 2)) 96 | const data = { 97 | getConvo: { 98 | ...prev.getConvo, 99 | messages: { 100 | ...prev.getConvo.messages, 101 | items: [newMsg, ...prev.getConvo.messages.items] 102 | } 103 | } 104 | } 105 | // console.log('view data', JSON.stringify(data, null, 2)) 106 | proxy.writeQuery({ ...QUERY, data }) 107 | } 108 | }) 109 | } 110 | 111 | export default class InputBar extends React.Component { 112 | state = { content: '', popoverOpen: false } 113 | handleChange = e => { 114 | this.setState({ content: e.target.value }) 115 | } 116 | 117 | handleFileChange = e => { 118 | const file = e.target.files[0] 119 | console.log(file) 120 | if (file && file.type.startsWith('image/')) { 121 | const reader = new FileReader() 122 | const self = this 123 | reader.onload = function(e) { 124 | self.setState({ popoverOpen: true, filePreviewSrc: this.result }) 125 | } 126 | reader.readAsDataURL(file) 127 | this.setState({ file }) 128 | } 129 | } 130 | 131 | handleSubmit = async e => { 132 | console.log('submit') 133 | e.preventDefault() 134 | 135 | const { content, file: selectedFile } = this.state 136 | const { conversation, userId, createMessage } = this.props 137 | 138 | if (content.trim().length === 0 && !selectedFile) { 139 | return 140 | } 141 | 142 | const file = await getFile(selectedFile) 143 | console.log('file for s3', file) 144 | 145 | const mutator = doCreateMessage( 146 | createMessage, 147 | content, 148 | file, 149 | conversation.id, 150 | userId 151 | ) 152 | 153 | this.setState({ content: '', file: undefined, popoverOpen: false }, mutator) 154 | } 155 | 156 | close = () => { 157 | this.setState({ file: undefined, popoverOpen: false }) 158 | } 159 | 160 | render() { 161 | const { filePreviewSrc } = this.state 162 | const disabled = !this.props.conversation ? { disabled: 'disabled' } : {} 163 | const imgBtnClass = 164 | 'btn btn-block ' + (this.state.file ? 'btn-success' : 'btn-primary') 165 | 166 | return ( 167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | 179 | 182 | 187 | 188 | Preview 189 | 197 | 198 | 199 | previw 204 | 205 | 206 |
207 |
208 | 216 |
217 |
218 |
219 |
220 | 221 |
222 |
223 | ) 224 | } 225 | } 226 | 227 | InputBar.propTypes = { 228 | createMessage: PropTypes.func, 229 | conversation: PropTypes.object, 230 | userId: PropTypes.string 231 | } 232 | 233 | const InputBarWithData = graphql(createMessage, { 234 | name: 'createMessage' 235 | })(InputBar) 236 | export { InputBarWithData } 237 | -------------------------------------------------------------------------------- /src/components/Message.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Popover, { ArrowContainer } from 'react-tiny-popover' 4 | import moment from 'moment' 5 | import Labels from './AI/DetectLabels' 6 | import Celebs from './AI/DetectCelebs' 7 | import Dictate from './AI/Dictate' 8 | import AIMenu from './AI/MenuDropDown' 9 | import TranslateCard from './AI/TranslateCard' 10 | import Bot from './AI/Bot' 11 | import DetectSentiment from './AI/DetectSentiment' 12 | import DetectEntities from './AI/DetectEntities' 13 | import InvokeBot from './AI/InvokeBot' 14 | import lex from '../images/lex.png' 15 | import chuck from '../images/chuck.jpg' 16 | import uuid from 'uuid/v4' 17 | import { Auth, Storage, Cache } from 'aws-amplify' 18 | import awsmobile from '../aws-exports' 19 | 20 | const VISIBILITY = 'protected' 21 | 22 | Storage.configure({ level: 'protected' }) 23 | 24 | function formatDate(date) { 25 | return moment(date).calendar(null, { 26 | sameDay: 'LT', 27 | lastDay: 'MMM D LT', 28 | lastWeek: 'MMM D LT', 29 | sameElse: 'l' 30 | }) 31 | } 32 | 33 | const BOTS = { 34 | CHUCKBOT: 'ChuckBot', 35 | MOVIEBOT: 'MovieBot' 36 | } 37 | 38 | const voiceMap = { 39 | en: 'Matthew', 40 | zh: 'Zhiyu', 41 | pt: 'Ricardo', 42 | fr: 'Mathieu', 43 | es: 'Miguel' 44 | } 45 | 46 | export default class Message extends React.Component { 47 | state = { 48 | fileUrl: undefined, 49 | bucket: awsmobile.aws_user_files_s3_bucket, 50 | key: null, 51 | popover: false, 52 | toDictate: null, 53 | translated: null, 54 | selectedLanguage: null, 55 | originalLanguage: null, 56 | chuckbot: null, 57 | bot: null, 58 | voice: null, 59 | dictate: false, 60 | detectLanguage: false, 61 | dropdownOpen: false, 62 | sentiment: false 63 | } 64 | componentDidMount() { 65 | const { msg: currMsg } = this.props 66 | const now = new Date().getTime() 67 | this.checkFileUrl() 68 | // console.log('mounted since, ', now - Date.parse(currMsg.createdAt)) 69 | if (now - Date.parse(currMsg.createdAt) < 200) { 70 | if (currMsg.content.includes('@chuckbot')) { 71 | this.setState({ bot: BOTS.CHUCKBOT, chuckbot: true }) 72 | } 73 | if (currMsg.content.includes('@moviebot')) { 74 | this.setState({ bot: BOTS.MOVIEBOT }) 75 | } 76 | } else { 77 | this.setState({ bot: null }) 78 | } 79 | } 80 | 81 | componentDidUpdate(prevProps, prevState) { 82 | const { msg: prevMsg } = prevProps 83 | const { msg: currMsg } = this.props 84 | if ( 85 | prevMsg.file && 86 | prevMsg.file.key === null && 87 | currMsg.file && 88 | currMsg.file.key 89 | ) { 90 | this.checkFileUrl() 91 | } 92 | } 93 | 94 | checkFileUrl() { 95 | const { file } = this.props.msg 96 | if (file && file.key) { 97 | const fileUrl = Cache.getItem(file.key) 98 | if (fileUrl) { 99 | console.log(`Retrieved cache url for ${file.key}: ${fileUrl}`) 100 | this.setState({ key: file.key }) 101 | return this.setState({ fileUrl }) 102 | } 103 | 104 | const [, identityIdWithSlash, keyWithoutPrefix] = 105 | /([^/]+\/){2}(.*)$/.exec(file.key) || file.key 106 | const identityId = identityIdWithSlash.replace(/\//g, '') 107 | console.log( 108 | `Retrieved new key for ${file.key}: ${identityId} - ${keyWithoutPrefix}` 109 | ) 110 | Storage.get(keyWithoutPrefix, { identityId }).then(fileUrl => { 111 | console.log(`New url for ${file.key}: ${fileUrl}`) 112 | const expires = moment() 113 | .add(14, 'm') 114 | .toDate() 115 | .getTime() 116 | Cache.setItem(file.key, fileUrl, { expires }) 117 | this.setState({ fileUrl }) 118 | }) 119 | } 120 | } 121 | 122 | getImageLabels(message) { 123 | this.setState({ popover: !this.state.popover, key: message.file.key }) 124 | } 125 | 126 | dictate = () => { 127 | this.doDictate(this.props.msg.content, true) 128 | } 129 | 130 | dictateTranslated = () => { 131 | this.doDictate(this.state.translated, false) 132 | } 133 | 134 | doDictate = async (message, original) => { 135 | console.log('Text to Dictate:' + message) 136 | const { identityId } = await Auth.currentCredentials() 137 | const key = `${VISIBILITY}/${identityId}/${uuid()}` 138 | const lang = original 139 | ? this.state.originalLanguage 140 | : this.state.selectedLanguage 141 | const voice = voiceMap[lang] || 'Russell' 142 | this.setState({ 143 | key: key, 144 | voice: voice, 145 | toDictate: message, 146 | dictate: true 147 | }) 148 | } 149 | 150 | finishedDictating = () => { 151 | this.setState({ dictate: false }) 152 | } 153 | 154 | comprehend = () => { 155 | this.setState({ sentiment: true }) 156 | } 157 | 158 | setLanguageCode = code => { 159 | this.setState({ originalLanguage: code }) 160 | } 161 | 162 | toggleDropDown = () => { 163 | this.setState({ dropdownOpen: !this.state.dropdownOpen }) 164 | } 165 | 166 | closeTranslateCard = () => { 167 | this.setState({ selectedLanguage: null, dictate: false }) 168 | } 169 | 170 | applyState = state => { 171 | this.setState(state) 172 | } 173 | 174 | render() { 175 | const { msg, username, ownsPrev, isUser } = this.props 176 | const { fileUrl, bucket, key, voice, toDictate, bot, popover } = this.state 177 | 178 | const outerClassName = 179 | 'd-inline-flex' + (isUser && !msg.chatbot ? '' : ' flex-row-reverse') 180 | const innerClassName = 181 | 'chatMsg shadow-sm pt-1 pb-1 px-2 rounded m-2 ' + 182 | (msg.chatbot 183 | ? 'bg-info text-white' 184 | : isUser 185 | ? 'bg-ember text-white' 186 | : 'bg-ampligygray text-white') 187 | const checkStatusClassName = 188 | 'ml-1 ' + (msg.isSent ? 'text-blue' : 'text-muted') 189 | 190 | return ( 191 |
192 |
193 |
194 | {!ownsPrev ? ( 195 |
{username}
196 | ) : null} 197 |
198 | 208 |
209 |
210 | {msg.file && 211 | (fileUrl ? ( 212 |
213 |
214 | awesome this.getImageLabels(msg)} 219 | id="ImgPopover" 220 | /> 221 |
222 |
223 | ( 228 | 236 |
237 |
238 | Image Rekognition 239 | 247 |
248 |
this.setState({ popover: !popover })} 256 | > 257 |
258 | 259 | 260 |
261 |
262 |
263 |
264 | )} 265 | > 266 |
267 | 268 |
269 |
270 | ) : ( 271 |
272 | ))} 273 |
274 | {msg.chatbot ? ( 275 |
276 |
277 |
278 | 279 | 280 | 281 |
282 |
283 | 284 | Amazon Lex 292 | {msg.content.match(/\[(\w+)\]/)[1]} 293 | 294 |
295 | {msg.content.startsWith(`[${BOTS.MOVIEBOT}]`) ? ( 296 | 297 | ) : ( 298 | msg.content.match(/\[\w+\]\s*(.*)/)[1] 299 | )} 300 | {msg.content.startsWith(`[${BOTS.CHUCKBOT}]`) ? ( 301 |
302 | Chuck Norris Facts 310 |
311 | ) : null} 312 |
313 |
314 |
315 |
316 | ) : ( 317 | msg.content 318 | )} 319 |
320 | {this.state.selectedLanguage ? ( 321 | 328 | ) : null} 329 | {this.state.dictate && ( 330 | 337 | )} 338 |
339 | {this.state.sentiment && ( 340 |
341 |
342 |
343 |
344 | 345 | Sentiment Analysis: 346 | {' '} 347 | 348 | 356 | 357 |
358 | 362 | 366 |
367 | )} 368 | {bot && ( 369 |
370 | 371 | 379 | 380 | 381 |
382 | )} 383 |
384 |
385 | {formatDate(msg.createdAt)} 386 | 387 | 388 | 389 |
390 |
391 |
392 | ) 393 | } 394 | } 395 | 396 | Message.propTypes = { 397 | msg: PropTypes.object.isRequired, 398 | username: PropTypes.string.isRequired, 399 | ownsPrev: PropTypes.bool.isRequired, 400 | isUser: PropTypes.bool.isRequired 401 | } 402 | -------------------------------------------------------------------------------- /src/components/MessagePane.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Message from './Message' 4 | import { Scrollbars } from 'react-custom-scrollbars' 5 | import { onCreateMessage } from '../graphql/subscriptions' 6 | import { Subject, of, from } from 'rxjs' 7 | import { pairwise, filter, exhaustMap } from 'rxjs/operators' 8 | import WordCloud from 'react-d3-cloud' 9 | import sizeMe from 'react-sizeme' 10 | 11 | const SCROLL_THRESHOLD = 0.25 12 | 13 | const fontSizeMapper = word => Math.log2(word.value) * 5 14 | const rotate = word => word.value % 360 15 | const worldCloud = [ 16 | { text: 'GraphQL', value: 15000 }, 17 | { text: 'AI', value: 2000 }, 18 | { text: 'Sentiment Analysis', value: 800 }, 19 | { text: 'ChatBots', value: 10000 }, 20 | { text: 'Translation', value: 300 }, 21 | { text: 'Media', value: 150 }, 22 | { text: 'Image Rekognition', value: 350 }, 23 | { text: 'Chuck Norris', value: 350 }, 24 | { text: 'Movies', value: 250 }, 25 | { text: 'English', value: 100 }, 26 | { text: 'Speech Recognition', value: 250 }, 27 | { text: 'Celebrity ReKognition', value: 200 }, 28 | { text: 'French', value: 100 }, 29 | { text: 'Mandarin', value: 100 }, 30 | { text: 'Spanish', value: 100 }, 31 | { text: 'Cloud', value: 500 }, 32 | { text: 'Real-Time', value: 700 }, 33 | { text: 'Offline', value: 700 }, 34 | { text: 'Lex', value: 800 }, 35 | { text: 'Polly', value: 800 }, 36 | { text: 'Comprehend', value: 800 }, 37 | { text: 'Translate', value: 800 }, 38 | { text: 'AppSync', value: 800 }, 39 | { text: 'DynamoDB', value: 800 }, 40 | { text: 'Elasticsearch', value: 800 }, 41 | { text: 'Welcome', value: 3500 }, 42 | { text: 'Cognito', value: 800 }, 43 | { text: 'Search', value: 500 }, 44 | { text: 'Serverless', value: 20000 }, 45 | { text: 'AWS', value: 50000 }, 46 | { text: '欢迎光临', value: 1500 }, 47 | { text: 'Bienvenue', value: 2500 }, 48 | { text: 'Bem-vindo', value: 1500 }, 49 | { text: 'Bienvenido', value: 1500 }, 50 | { text: 'NoSQL', value: 2500 }, 51 | { text: 'Screen', value: 5500 }, 52 | { text: 'Mobility', value: 7500 }, 53 | { text: 'Progressive', value: 9500 }, 54 | { text: 'PWAs', value: 9500 }, 55 | { text: 'Data', value: 90500 }, 56 | { text: 'Amplify', value: 10500 } 57 | ] 58 | 59 | class MessagePane extends React.Component { 60 | state = { 61 | width: 691, 62 | height: 650 63 | } 64 | scrollbarsRef = React.createRef() 65 | subject = new Subject() 66 | obs = this.subject.asObservable() 67 | 68 | componentDidMount() { 69 | console.log('MessagePane - componentDidMount') 70 | if (this.props.conversation) { 71 | console.log('MessagePane - componentDidMount - subscribe') 72 | this.unsubscribe = this.createSubForConvoMsgs() 73 | } 74 | this.obs 75 | .pipe( 76 | pairwise(), 77 | filter(this.isScrollingUpPastThreshold), 78 | exhaustMap(this.loadMoreMessages) 79 | ) 80 | .subscribe(_ => {}) 81 | this.getDimensions(this.props.size) 82 | } 83 | 84 | componentDidUpdate(prevProps, prevState) { 85 | const currConvo = this.props.conversation || {} 86 | const prevConvo = prevProps.conversation || {} 87 | if (currConvo && prevConvo.id !== currConvo.id) { 88 | if (this.unsubscribe) { 89 | console.log('MessagePane - componentDidUpdate - unsubscribe') 90 | this.unsubscribe() 91 | } 92 | console.log('MessagePane - componentDidUpdate - subscribe') 93 | this.unsubscribe = this.createSubForConvoMsgs() 94 | } 95 | const prevMsgs = prevProps.messages || [] 96 | const messages = this.props.messages || [] 97 | if (prevMsgs.length !== messages.length) { 98 | const p0 = prevMsgs[0] 99 | const m0 = messages[0] 100 | if ((p0 && m0 && p0.id !== m0.id) || (!p0 && m0)) { 101 | this.scrollbarsRef.current.scrollToBottom() 102 | } 103 | } 104 | } 105 | 106 | componentWillUnmount() { 107 | console.log('MessagePane - componentWillUnmount') 108 | if (this.unsubscribe) { 109 | console.log('MessagePane - componentDidUpdate - unsubscribe') 110 | this.unsubscribe() 111 | } 112 | } 113 | 114 | getDimensions(size) { 115 | console.log(size) 116 | if (size.width > 750) { 117 | this.setState({ height: size.height }) 118 | } else { 119 | this.setState({ width: size.width, height: size.height }) 120 | } 121 | } 122 | 123 | isScrollingUpPastThreshold = ([prev, curr]) => { 124 | // console.log('isScrolling', prev, curr) 125 | const result = (prev.top > curr.top) & (curr.top < SCROLL_THRESHOLD) 126 | if (result) { 127 | console.log('Should fetch more messages') 128 | } 129 | return result 130 | } 131 | 132 | loadMoreMessages = () => { 133 | const { fetchMore, nextToken } = this.props 134 | if (!nextToken) { 135 | return of(true) 136 | } 137 | const result = fetchMore({ 138 | variables: { nextToken: nextToken }, 139 | updateQuery: (prev, { fetchMoreResult: data }) => { 140 | const update = { 141 | getConvo: { 142 | ...prev.getConvo, 143 | messages: { 144 | ...prev.getConvo.messages, 145 | nextToken: data.getConvo.messages.nextToken, 146 | items: [ 147 | ...prev.getConvo.messages.items, 148 | ...data.getConvo.messages.items 149 | ] 150 | } 151 | } 152 | } 153 | return update 154 | } 155 | }) 156 | return from(result) 157 | } 158 | 159 | createSubForConvoMsgs = () => { 160 | const { 161 | subscribeToMore, 162 | conversation: { id: convoId }, 163 | userId 164 | } = this.props 165 | return subscribeToMore({ 166 | document: onCreateMessage, 167 | variables: { messageConversationId: convoId }, 168 | updateQuery: ( 169 | prev, 170 | { 171 | subscriptionData: { 172 | data: { onCreateMessage: newMsg } 173 | } 174 | } 175 | ) => { 176 | console.log('updateQuery on message subscription', prev, newMsg) 177 | if (newMsg.chatbot) { 178 | prev.getConvo.messages.items.forEach(function iterator(item, i) { 179 | if (newMsg.content === item.content) { 180 | console.log( 181 | 'repeated chatbot messages on position ' + 182 | i + 183 | ': ' + 184 | item.content 185 | ) 186 | } 187 | }) 188 | } 189 | if (newMsg.owner === userId && !newMsg.chatbot) { 190 | console.log('skipping own message') 191 | return 192 | } 193 | 194 | const current = { 195 | getConvo: { 196 | ...prev.getConvo, 197 | messages: { 198 | ...prev.getConvo.messages, 199 | items: [newMsg, ...prev.getConvo.messages.items] 200 | } 201 | } 202 | } 203 | return current 204 | } 205 | }) 206 | } 207 | 208 | render() { 209 | const { messages, conversation, userMap, userId } = this.props 210 | return ( 211 |
212 | {conversation ? ( 213 | this.subject.next(values)} 218 | ref={this.scrollbarsRef} 219 | > 220 |
221 | {[...messages].reverse().map((msg, idx, arr) => ( 222 | 0 && arr[idx - 1].owner === msg.owner} 226 | isUser={msg.owner === userId} 227 | key={msg.id} 228 | /> 229 | ))} 230 |
231 |
232 | ) : ( 233 |
234 | 242 |
243 | )} 244 |
245 | ) 246 | } 247 | } 248 | MessagePane.propTypes = { 249 | conversation: PropTypes.object, 250 | userId: PropTypes.string, 251 | messages: PropTypes.array.isRequired, 252 | userMap: PropTypes.object.isRequired, 253 | subscribeToMore: PropTypes.func, 254 | fetchMore: PropTypes.func, 255 | nextToken: PropTypes.string 256 | } 257 | export default sizeMe({ monitorHeight: true })(MessagePane) 258 | -------------------------------------------------------------------------------- /src/components/Messenger.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import ConversationBar from './ConversationBar' 4 | import { InputBarWithData } from './InputBar' 5 | import MessagePane from './MessagePane' 6 | import { getConvo } from '../graphql/queries' 7 | import { graphql } from 'react-apollo' 8 | 9 | class Messenger extends Component { 10 | render() { 11 | const { 12 | switchView, 13 | conversation, 14 | conversationName, 15 | data: { 16 | subscribeToMore, 17 | fetchMore, 18 | getConvo: { messages: { items: messages = [], nextToken } = {} } = {} 19 | } = {} 20 | } = this.props 21 | return ( 22 | 23 | 26 | 32 | 36 | 37 | ) 38 | } 39 | 40 | getUserMap = () => { 41 | const { 42 | conversation: { associated: { items = [] } = {} } = {} 43 | } = this.props 44 | return items.reduce((acc, curr) => { 45 | acc[curr.user.id] = curr.user.username 46 | return acc 47 | }, {}) 48 | } 49 | } 50 | 51 | Messenger.propTypes = { 52 | conversation: PropTypes.object, 53 | conversationName: PropTypes.string, 54 | userId: PropTypes.string, 55 | switchView: PropTypes.func.isRequired, 56 | data: PropTypes.object 57 | } 58 | 59 | const MessengerWithData = graphql(getConvo, { 60 | skip: props => !props.conversation, 61 | options: props => ({ 62 | variables: { id: props.conversation.id }, 63 | fetchPolicy: 'cache-and-network' 64 | }) 65 | })(Messenger) 66 | 67 | export default Messenger 68 | export { MessengerWithData } 69 | -------------------------------------------------------------------------------- /src/components/SearchResultList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { searchMessages, searchUsers } from '../graphql/queries' 4 | import { graphql, compose } from 'react-apollo' 5 | import BarLoader from 'react-spinners/BarLoader' 6 | import moment from 'moment' 7 | 8 | function formatDate(date) { 9 | return moment(date).calendar(null, { 10 | sameDay: 'LT', 11 | lastDay: 'MMM D LT', 12 | lastWeek: 'MMM D LT', 13 | sameElse: 'l' 14 | }) 15 | } 16 | 17 | const SearchResultList = ({ 18 | getMenuProps, 19 | getItemProps, 20 | selectedItem, 21 | userSearchData = {}, 22 | msgSearchData = {}, 23 | conversations = { items: [] } 24 | }) => { 25 | const { 26 | loading: uLoading = false, 27 | searchUsers: { items: users = [] } = {} 28 | } = userSearchData 29 | const { 30 | loading: mLoading = false, 31 | searchMessages: { items: messages = [] } = {} 32 | } = msgSearchData 33 | 34 | const data = { users, messages } 35 | 36 | // console.log(JSON.stringify(data, null, 2)) 37 | const convoMap = conversations.items.reduce((acc, cur) => { 38 | acc[cur.conversation.id] = cur.name 39 | return acc 40 | }, {}) 41 | return ( 42 |
43 |
44 | 45 | 46 |
47 |
48 | ) 49 | } 50 | SearchResultList.propTypes = { 51 | getMenuProps: PropTypes.func.isRequired, 52 | getItemProps: PropTypes.func.isRequired, 53 | conversations: PropTypes.object, 54 | selectedItem: PropTypes.object, 55 | term: PropTypes.string, 56 | userSearchData: PropTypes.object, 57 | msgSearchData: PropTypes.object 58 | } 59 | 60 | const DataLoading = ({ isLoading = false }) => ( 61 |
62 | 72 |
73 | ) 74 | DataLoading.propTypes = { 75 | isLoading: PropTypes.bool.isRequired 76 | } 77 | 78 | const ResultLists = ({ 79 | data: { users, messages }, 80 | getItemProps, 81 | selectedItem, 82 | convoMap 83 | }) => { 84 | const totalLength = users.length + messages.length 85 | return ( 86 | 87 | {totalLength ? ( 88 | 89 | 90 | 99 | 100 | ) : ( 101 |
Nothing found
102 | )} 103 |
104 | ) 105 | } 106 | ResultLists.propTypes = { 107 | getItemProps: PropTypes.func.isRequired, 108 | data: PropTypes.object, 109 | selectedItem: PropTypes.object, 110 | convoMap: PropTypes.object.isRequired 111 | } 112 | 113 | const UserList = ({ users, offset, getItemProps, selectedItem }) => { 114 | const selItemId = selectedItem ? selectedItem.id : null 115 | return ( 116 | 117 |
118 |
119 |

Users

120 |
121 | {!users.length && ( 122 |
123 | Nothing found 124 |
125 | )} 126 | {users.map((user, index) => ( 127 | 141 | ))} 142 |
143 |
144 | ) 145 | } 146 | UserList.propTypes = { 147 | getItemProps: PropTypes.func.isRequired, 148 | users: PropTypes.array.isRequired, 149 | selectedItem: PropTypes.object, 150 | offset: PropTypes.number.isRequired 151 | } 152 | 153 | const MessageList = ({ 154 | messages, 155 | offset, 156 | getItemProps, 157 | selectedItem, 158 | convoMap 159 | }) => { 160 | const selItemId = selectedItem ? selectedItem.id : null 161 | return ( 162 | 163 |
164 |
165 |

Messages

166 |
167 | {!messages.length && ( 168 |
169 | Nothing found 170 |
171 | )} 172 | {messages.map((msg, index) => ( 173 | 191 | ))} 192 |
193 |
194 | ) 195 | } 196 | MessageList.propTypes = { 197 | getItemProps: PropTypes.func.isRequired, 198 | messages: PropTypes.array.isRequired, 199 | offset: PropTypes.number.isRequired, 200 | selectedItem: PropTypes.object, 201 | convoMap: PropTypes.object.isRequired 202 | } 203 | 204 | function buildMsgFilter(term, conversations = {}) { 205 | const items = conversations.items || [] 206 | const convoIds = items.map(i => i.conversation.id) 207 | const filter = { 208 | content: { regexp: `.*${term}.*` }, 209 | and: [ 210 | { 211 | or: convoIds.map(id => ({ messageConversationId: { eq: id } })) 212 | } 213 | ] 214 | } 215 | 216 | return filter 217 | } 218 | 219 | const SearchResultListWithData = compose( 220 | graphql(searchUsers, { 221 | name: 'userSearchData', 222 | skip: props => !props.term, 223 | options: props => ({ 224 | variables: { 225 | filter: { username: { regexp: `.*${props.term}.*` } } 226 | }, 227 | fetchPolicy: 'cache-and-network' 228 | }) 229 | }), 230 | graphql(searchMessages, { 231 | name: 'msgSearchData', 232 | skip: props => 233 | !props.term || !props.conversations || !props.conversations.items.length, 234 | options: props => ({ 235 | variables: { 236 | filter: buildMsgFilter(props.term, props.conversations) 237 | }, 238 | fetchPolicy: 'cache-and-network' 239 | }) 240 | }) 241 | )(SearchResultList) 242 | 243 | export default SearchResultList 244 | export { SearchResultListWithData } 245 | -------------------------------------------------------------------------------- /src/components/SideBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Downshift from 'downshift' 3 | import { Scrollbars } from 'react-custom-scrollbars' 4 | import PropTypes from 'prop-types' 5 | import { onUpdateConvoLink } from '../graphql/subscriptions' 6 | import _cloneDeep from 'lodash.clonedeep' 7 | import _debounce from 'lodash.debounce' 8 | import SideList from './ConvoSideList' 9 | import { SearchResultListWithData } from './SearchResultList' 10 | 11 | const SearchBar = ({ propsFn }) => ( 12 |
13 |
14 |
15 |
16 | 21 |
22 |
23 |
24 |
25 | ) 26 | SearchBar.propTypes = { 27 | propsFn: PropTypes.func.isRequired 28 | } 29 | 30 | export default class SideBar extends React.Component { 31 | state = { 32 | searchTerm: null 33 | } 34 | 35 | componentDidMount() { 36 | if (this.props.userId) { 37 | this.unsubscribe = createSubForConvoList( 38 | this.props.subscribeToMore, 39 | this.props.userId 40 | ) 41 | } 42 | } 43 | 44 | componentDidUpdate(prevProps, prevState) { 45 | if (this.props.userId && !this.unsubscribe) { 46 | this.unsubscribe = createSubForConvoList( 47 | this.props.subscribeToMore, 48 | this.props.userId 49 | ) 50 | } 51 | } 52 | 53 | componentWillUnmount() { 54 | if (this.unsubscribe) { 55 | this.unsubscribe() 56 | } 57 | } 58 | 59 | onStateChange = _debounce( 60 | ({ inputValue }) => { 61 | if (typeof inputValue !== 'undefined') { 62 | this.setState({ searchTerm: inputValue.trim() }) 63 | } 64 | }, 65 | 250, 66 | { maxWait: 500 } 67 | ) 68 | 69 | onChange = selection => { 70 | this.props.onChange(selection) 71 | // clear search 72 | } 73 | 74 | stateReducer = (state, changes) => { 75 | // console.log(state, changes) 76 | switch (changes.type) { 77 | case Downshift.stateChangeTypes.blurInput: 78 | case Downshift.stateChangeTypes.mouseUp: 79 | case Downshift.stateChangeTypes.keyDownEnter: 80 | case Downshift.stateChangeTypes.itemMouseEnter: 81 | // console.log('ignore', changes.type) 82 | return { 83 | ...changes, 84 | isOpen: state.isOpen, 85 | inputValue: state.inputValue 86 | } 87 | case Downshift.stateChangeTypes.keyDownEscape: 88 | return { ...changes, isOpen: false, inputValue: '' } 89 | default: 90 | return changes 91 | } 92 | } 93 | 94 | render() { 95 | const conversations = this.props.conversations 96 | return ( 97 | (item ? '' : '')} 102 | stateReducer={this.stateReducer} 103 | > 104 | {({ 105 | getInputProps, 106 | getItemProps, 107 | getLabelProps, 108 | getMenuProps, 109 | isOpen, 110 | inputValue, 111 | highlightedIndex, 112 | selectedItem 113 | }) => ( 114 |
115 | 116 |
117 | 118 | {isOpen ? ( 119 | 128 | ) : ( 129 | 137 | )} 138 | 139 |
140 |
141 | )} 142 |
143 | ) 144 | } 145 | } 146 | SideBar.propTypes = { 147 | userId: PropTypes.string, 148 | subscribeToMore: PropTypes.func, 149 | conversations: PropTypes.object, 150 | onChange: PropTypes.func.isRequired 151 | } 152 | 153 | function createSubForConvoList(subscribeToMore, userId) { 154 | return subscribeToMore({ 155 | document: onUpdateConvoLink, 156 | variables: { convoLinkUserId: userId, status: 'READY' }, 157 | updateQuery: ( 158 | prev, 159 | { 160 | subscriptionData: { 161 | data: { onUpdateConvoLink: newConvo } 162 | } 163 | } 164 | ) => { 165 | console.log('updateQuery on convo subscription', prev, newConvo) 166 | const current = _cloneDeep(prev) 167 | current.getUser.userConversations.items.unshift(newConvo) 168 | return current 169 | } 170 | }) 171 | } 172 | -------------------------------------------------------------------------------- /src/components/UserBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const UserBar = ({ name, registered, signout, switchView }) => ( 5 |
6 | 25 |
26 | ); 27 | UserBar.propTypes = { 28 | name: PropTypes.string, 29 | registered: PropTypes.bool, 30 | switchView: PropTypes.func.isRequired, 31 | signout: PropTypes.func.isRequired 32 | }; 33 | 34 | export default UserBar; 35 | -------------------------------------------------------------------------------- /src/components/chatapp.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Auth } from 'aws-amplify' 4 | import { graphql, compose } from 'react-apollo' 5 | import { getUser } from '../graphql/queries' 6 | import { 7 | registerUser, 8 | createConvo, 9 | createConvoLink, 10 | updateConvoLink 11 | } from '../graphql/mutations' 12 | import UserBar from './UserBar' 13 | import SideBar from './SideBar' 14 | import { MessengerWithData } from './Messenger' 15 | 16 | function chatName(userName) { 17 | return `${userName} (chat)` 18 | } 19 | 20 | const convoList = {} 21 | 22 | class ChatApp extends Component { 23 | state = { 24 | conversation: undefined, 25 | registered: false, 26 | viewCN: false 27 | } 28 | 29 | signout = e => { 30 | e.preventDefault() 31 | Auth.signOut() 32 | .then(data => window.location.reload()) 33 | .catch(err => console.log(err)) 34 | } 35 | 36 | componentDidUpdate(prevProps, prevState, snapshot) { 37 | const { data: { loading, getUser } = {} } = this.props 38 | console.log('run register - before', loading, getUser) 39 | if (!loading && !getUser) { 40 | console.log('run register', this.props.id, this.props.data.loading) 41 | this.props.registerUser() 42 | } 43 | } 44 | 45 | initConvo = selection => { 46 | console.log('initConvo', selection) 47 | switch (selection.__typename) { 48 | case 'User': 49 | return this.startConvoWithUser({ user: selection }) 50 | case 'ConvoLink': 51 | return this.gotoConversation({ convoLink: selection }) 52 | case 'Message': 53 | return this.startConvoAtMessage({ message: selection }) 54 | default: 55 | break 56 | } 57 | } 58 | 59 | startConvoWithUser = async ({ user }) => { 60 | let conversationInfo = this.findConverationWithUser(user) 61 | if (!conversationInfo) { 62 | console.log('no convo, launch new') 63 | conversationInfo = await this.launchNewConversation(user) 64 | } 65 | console.log('Got the convo', conversationInfo) 66 | this.setState({ ...conversationInfo, viewCN: false }) 67 | } 68 | 69 | gotoConversation = ({ convoLink }) => { 70 | console.log('goto', convoLink.conversation) 71 | this.setState({ 72 | conversation: convoLink.conversation, 73 | conversationName: convoLink.name, 74 | viewCN: false 75 | }) 76 | } 77 | 78 | startConvoAtMessage = ({ message }) => { 79 | const { 80 | data: { 81 | getUser: { userConversations: { items: convoLinks = [] } = {} } = {} 82 | } = {} 83 | } = this.props 84 | const convoLink = convoLinks.find( 85 | c => c.conversation.id === message.messageConversationId 86 | ) 87 | if (convoLink) { 88 | this.setState({ 89 | conversation: convoLink.conversation, 90 | conversationName: convoLink.name, 91 | viewCN: false 92 | }) 93 | } 94 | } 95 | 96 | findConverationWithUser = user => { 97 | const { 98 | data: { 99 | getUser: { userConversations: { items: convoLinks = [] } = {} } = {} 100 | } = {} 101 | } = this.props 102 | const convoLink = convoLinks.find(c => { 103 | const { 104 | conversation: { associated: { items: assoc = [] } = {} } = {} 105 | } = c 106 | return assoc.some(a => a.convoLinkUserId === user.id) 107 | }) 108 | return convoLink 109 | ? { 110 | conversation: convoLink.conversation, 111 | conversationName: convoLink.name 112 | } 113 | : null 114 | } 115 | 116 | launchNewConversation = user => { 117 | let resolveFn 118 | const promise = new Promise((resolve, reject) => { 119 | resolveFn = resolve 120 | }) 121 | 122 | this.props.createConvo({ 123 | update: async (proxy, { data: { createConvo } }) => { 124 | console.log('update, ', createConvo) 125 | if (createConvo.id === '-1' || convoList[`${createConvo.id}`]) { 126 | return 127 | } 128 | convoList[`${createConvo.id}`] = true 129 | const me = this.props.data.getUser 130 | const otherChatName = chatName(me.username) 131 | const myChatName = chatName(user.username) 132 | const links = await Promise.all([ 133 | this.linkNewConversation(createConvo.id, user.id, otherChatName), 134 | this.linkNewConversation(createConvo.id, me.id, myChatName) 135 | ]) 136 | console.log('next steps', links) 137 | const promises = links.map(c => this.updateToReadyConversation(c)) 138 | const convoLinks = await Promise.all(promises) 139 | resolveFn({ 140 | conversation: convoLinks[0].conversation, 141 | conversationName: myChatName 142 | }) 143 | } 144 | }) 145 | return promise 146 | } 147 | 148 | linkNewConversation = (convoId, userId, chatName) => { 149 | console.log('linkNewConversation - start', convoId, userId, chatName) 150 | 151 | let resolveFn 152 | const promise = new Promise((resolve, reject) => { 153 | resolveFn = resolve 154 | }) 155 | this.props.createConvoLink({ 156 | variables: { convoId, userId, name: chatName }, 157 | optimisticResponse: { 158 | createConvoLink: { 159 | __typename: 'ConvoLink', 160 | id: '-1', 161 | status: 'PENDING', 162 | name: chatName, 163 | conversation: { 164 | __typename: 'Conversation', 165 | id: convoId, 166 | name: '', 167 | createdAt: '', 168 | associated: { 169 | __typename: 'ModelConvoLinkConnection', 170 | items: [] 171 | } 172 | } 173 | } 174 | }, 175 | update: async (proxy, { data: { createConvoLink } }) => { 176 | if (createConvoLink.id === '-1') { 177 | return 178 | } 179 | resolveFn(createConvoLink) 180 | } 181 | }) 182 | return promise 183 | } 184 | 185 | updateToReadyConversation = convoLink => { 186 | console.log('updateToReadyConversation - update', convoLink) 187 | 188 | let resolveFn 189 | const promise = new Promise((resolve, reject) => { 190 | resolveFn = resolve 191 | }) 192 | 193 | this.props.updateConvoLink({ 194 | variables: { id: convoLink.id }, 195 | optimisticResponse: { 196 | updateConvoLink: { 197 | __typename: 'ConvoLink', 198 | id: convoLink.id, 199 | name: convoLink.name, 200 | convoLinkUserId: '-1', 201 | status: 'CONFIRMING', 202 | conversation: { 203 | __typename: 'Conversation', 204 | id: convoLink.conversation.id, 205 | name: '', 206 | createdAt: '', 207 | associated: { 208 | __typename: 'ModelConvoLinkConnection', 209 | items: [] 210 | } 211 | } 212 | } 213 | }, 214 | update: async (proxy, { data: { updateConvoLink } }) => { 215 | console.log('confirmLink , ', updateConvoLink) 216 | if (updateConvoLink.status === 'READY') { 217 | resolveFn(updateConvoLink) 218 | } 219 | } 220 | }) 221 | return promise 222 | } 223 | 224 | switchView = () => { 225 | this.setState({ viewCN: !this.state.viewCN }) 226 | } 227 | 228 | render() { 229 | let { data: { subscribeToMore, getUser: user = {} } = {} } = this.props 230 | user = user || { name: '', registered: false } 231 | 232 | let cn = this.state.viewCN ? 'switchview' : '' 233 | cn += 234 | ' ' + 235 | 'bg-secondary row no-gutters align-items-stretch w-100 h-100 position-absolute' 236 | 237 | return ( 238 |
239 |
240 |
241 | 247 | 255 |
256 |
257 |
258 | 264 |
265 |
266 | ) 267 | } 268 | } 269 | ChatApp.propTypes = { 270 | name: PropTypes.string, 271 | id: PropTypes.string, 272 | data: PropTypes.object, 273 | registerUser: PropTypes.func.isRequired, 274 | createConvo: PropTypes.func.isRequired, 275 | createConvoLink: PropTypes.func.isRequired, 276 | updateConvoLink: PropTypes.func.isRequired 277 | } 278 | 279 | const ChatAppWithData = compose( 280 | graphql(getUser, { 281 | skip: props => !props.id, 282 | options: props => ({ 283 | variables: { id: props.id }, 284 | fetchPolicy: 'cache-and-network' 285 | }) 286 | }), 287 | graphql(registerUser, { 288 | name: 'registerUser', 289 | options: props => ({ 290 | variables: { 291 | input: { 292 | id: props.id, 293 | username: props.name, 294 | registered: true 295 | } 296 | }, 297 | optimisticResponse: { 298 | registerUser: { 299 | id: props.id, 300 | username: 'Standby', 301 | registered: false, 302 | __typename: 'User', 303 | userConversations: { 304 | __typename: 'ModelConvoLinkConnection', 305 | items: [] 306 | } 307 | } 308 | }, 309 | update: (proxy, { data: { registerUser } }) => { 310 | const QUERY = { 311 | query: getUser, 312 | variables: { id: props.id } 313 | } 314 | const prev = proxy.readQuery(QUERY) 315 | console.log('prev getUser', prev) 316 | const data = { 317 | ...prev, 318 | getUser: { ...registerUser } 319 | } 320 | proxy.writeQuery({ ...QUERY, data }) 321 | } 322 | }) 323 | }), 324 | graphql(createConvo, { 325 | name: 'createConvo', 326 | options: props => ({ 327 | ignoreResults: true, 328 | variables: { 329 | input: { name: 'direct' } 330 | }, 331 | optimisticResponse: { 332 | createConvo: { 333 | id: '-1', 334 | name: 'direct', 335 | createdAt: '', 336 | __typename: 'Conversation', 337 | associated: { 338 | __typename: 'ModelConvoLinkConnection', 339 | items: [] 340 | } 341 | } 342 | } 343 | }) 344 | }), 345 | graphql(createConvoLink, { 346 | name: 'createConvoLink', 347 | options: props => ({ 348 | ignoreResults: true 349 | }) 350 | }), 351 | graphql(updateConvoLink, { 352 | name: 'updateConvoLink', 353 | options: props => ({ 354 | ignoreResults: true 355 | }) 356 | }) 357 | )(ChatApp) 358 | 359 | export default ChatApp 360 | export { ChatAppWithData } 361 | -------------------------------------------------------------------------------- /src/graphql/AI/detectCelebs.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | query detectCelebs($bucket: String, $key: String) { 5 | detectCelebs(bucket: $bucket, key: $key) { 6 | response 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/graphql/AI/detectEntities.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | query detectEntities($language: String, $text: String) { 5 | detectEntities(language: $language, text: $text) { 6 | response 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/graphql/AI/detectLabels.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | query detectLabels($bucket: String, $key: String) { 5 | detectLabels(bucket: $bucket, key: $key) { 6 | response 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/graphql/AI/detectLanguage.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | query detectLanguage($text: String) { 5 | detectLanguage(text: $text) { 6 | response 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/graphql/AI/detectSentiment.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | query detectSentiment($language: String, $text: String) { 5 | detectSentiment(language: $language, text: $text) { 6 | response 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/graphql/AI/dictate.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | query dictate($bucket: String, $key: String, $voice: String, $text: String) { 5 | dictate(bucket: $bucket, key: $key, voice: $voice, text: $text) { 6 | response 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/graphql/AI/invokeBot.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | query invokeBot($bot: String, $text: String) { 5 | invokeBot(bot: $bot, text: $text) { 6 | response 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/graphql/AI/translate.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | query translate($language: String, $text: String) { 5 | translate(language: $language, text: $text) { 6 | response 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/graphql/mutations.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | 3 | export const createConvo = gql` 4 | mutation CreateConvo($input: CreateConversationInput!) { 5 | createConvo(input: $input) { 6 | id 7 | name 8 | createdAt 9 | associated { 10 | items { 11 | convoLinkUserId 12 | user { 13 | id 14 | username 15 | } 16 | } 17 | } 18 | } 19 | } 20 | ` 21 | export const createMessage = gql` 22 | mutation CreateMessage($input: CreateMessageInput!) { 23 | createMessage(input: $input) { 24 | id 25 | content 26 | createdAt 27 | owner 28 | chatbot 29 | isSent 30 | file { 31 | bucket 32 | region 33 | key 34 | } 35 | messageConversationId 36 | conversation { 37 | id 38 | name 39 | createdAt 40 | } 41 | } 42 | } 43 | ` 44 | export const registerUser = gql` 45 | mutation RegisterUser($input: CreateUserInput!) { 46 | registerUser(input: $input) { 47 | id 48 | username 49 | registered 50 | userConversations { 51 | items { 52 | id 53 | name 54 | status 55 | conversation { 56 | id 57 | name 58 | createdAt 59 | associated { 60 | items { 61 | convoLinkUserId 62 | user { 63 | id 64 | username 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | ` 74 | export const createConvoLink = gql` 75 | mutation createConvoLink($userId: ID!, $convoId: ID!, $name: String!) { 76 | createConvoLink( 77 | input: { 78 | convoLinkUserId: $userId 79 | convoLinkConversationId: $convoId 80 | name: $name 81 | status: "CREATING" 82 | } 83 | ) { 84 | id 85 | name 86 | status 87 | conversation { 88 | id 89 | name 90 | createdAt 91 | associated { 92 | items { 93 | convoLinkUserId 94 | user { 95 | id 96 | username 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | ` 104 | export const updateConvoLink = gql` 105 | mutation updateConvoLink($id: ID!) { 106 | updateConvoLink(input: { id: $id, status: "READY" }) { 107 | id 108 | name 109 | convoLinkUserId 110 | status 111 | conversation { 112 | id 113 | name 114 | createdAt 115 | associated { 116 | items { 117 | convoLinkUserId 118 | user { 119 | id 120 | username 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | ` 128 | -------------------------------------------------------------------------------- /src/graphql/queries.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | 3 | export const getUser = gql` 4 | query GetUser($id: ID!) { 5 | getUser(id: $id) { 6 | id 7 | username 8 | registered 9 | userConversations { 10 | items { 11 | id 12 | name 13 | status 14 | conversation { 15 | id 16 | name 17 | createdAt 18 | associated { 19 | items { 20 | convoLinkUserId 21 | user { 22 | id 23 | username 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | ` 33 | 34 | export const getConvo = gql` 35 | query GetConvo($id: ID!, $nextToken: String) { 36 | getConvo(id: $id) { 37 | id 38 | messages(sortDirection: DESC, limit: 20, nextToken: $nextToken) { 39 | nextToken 40 | items { 41 | id 42 | content 43 | createdAt 44 | owner 45 | chatbot 46 | isSent 47 | file { 48 | bucket 49 | region 50 | key 51 | } 52 | messageConversationId 53 | conversation { 54 | id 55 | name 56 | createdAt 57 | } 58 | } 59 | } 60 | } 61 | } 62 | ` 63 | 64 | export const searchMessages = gql` 65 | query SearchMessages( 66 | $filter: SearchableMessageFilterInput 67 | $sort: SearchableMessageSortInput 68 | $limit: Int 69 | $nextToken: Int 70 | ) { 71 | searchMessages( 72 | filter: $filter 73 | sort: $sort 74 | limit: $limit 75 | nextToken: $nextToken 76 | ) { 77 | items { 78 | id 79 | content 80 | createdAt 81 | owner 82 | chatbot 83 | isSent 84 | file { 85 | bucket 86 | region 87 | key 88 | } 89 | messageConversationId 90 | conversation { 91 | id 92 | name 93 | createdAt 94 | } 95 | } 96 | nextToken 97 | } 98 | } 99 | ` 100 | export const searchUsers = gql` 101 | query SearchUsers( 102 | $filter: SearchableUserFilterInput 103 | $sort: SearchableUserSortInput 104 | $limit: Int 105 | $nextToken: Int 106 | ) { 107 | searchUsers( 108 | filter: $filter 109 | sort: $sort 110 | limit: $limit 111 | nextToken: $nextToken 112 | ) { 113 | items { 114 | id 115 | username 116 | registered 117 | userConversations { 118 | items { 119 | id 120 | name 121 | status 122 | convoLinkUserId 123 | } 124 | nextToken 125 | } 126 | } 127 | nextToken 128 | } 129 | } 130 | ` 131 | export const searchConvoLinks = gql` 132 | query SearchConvoLinks( 133 | $filter: SearchableConvoLinkFilterInput 134 | $sort: SearchableConvoLinkSortInput 135 | $limit: Int 136 | $nextToken: Int 137 | ) { 138 | searchConvoLinks( 139 | filter: $filter 140 | sort: $sort 141 | limit: $limit 142 | nextToken: $nextToken 143 | ) { 144 | items { 145 | id 146 | name 147 | status 148 | convoLinkUserId 149 | user { 150 | id 151 | username 152 | registered 153 | } 154 | conversation { 155 | id 156 | name 157 | createdAt 158 | } 159 | } 160 | nextToken 161 | } 162 | } 163 | ` 164 | -------------------------------------------------------------------------------- /src/graphql/subscriptions.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | 3 | export const onCreateMessage = gql` 4 | subscription OnCreateMessage($messageConversationId: ID!) { 5 | onCreateMessage(messageConversationId: $messageConversationId) { 6 | id 7 | content 8 | createdAt 9 | owner 10 | chatbot 11 | isSent 12 | file { 13 | bucket 14 | region 15 | key 16 | } 17 | messageConversationId 18 | conversation { 19 | id 20 | name 21 | createdAt 22 | } 23 | } 24 | } 25 | ` 26 | export const onUpdateConvoLink = gql` 27 | subscription OnUpdateConvoLink($convoLinkUserId: ID, $status: String) { 28 | onUpdateConvoLink(convoLinkUserId: $convoLinkUserId, status: $status) { 29 | id 30 | name 31 | status 32 | conversation { 33 | id 34 | name 35 | createdAt 36 | associated { 37 | items { 38 | convoLinkUserId 39 | user { 40 | id 41 | username 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | ` 49 | -------------------------------------------------------------------------------- /src/images/appsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/src/images/appsync.png -------------------------------------------------------------------------------- /src/images/chuck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/src/images/chuck.jpg -------------------------------------------------------------------------------- /src/images/comprehend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/src/images/comprehend.png -------------------------------------------------------------------------------- /src/images/lex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/src/images/lex.png -------------------------------------------------------------------------------- /src/images/lexlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/src/images/lexlogo.png -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/src/images/logo.png -------------------------------------------------------------------------------- /src/images/polly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/src/images/polly.png -------------------------------------------------------------------------------- /src/images/rekognition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/src/images/rekognition.png -------------------------------------------------------------------------------- /src/images/translate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-chat-starter-react/34c7422a4115e8247e360d38d286bba2b14d236c/src/images/translate.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | $blue: #00b9f2; 2 | $indigo: #007eb9; 3 | $purple: #527fff; 4 | // $pink: 5 | $red: #dd3f5b; 6 | $orange: #ff9900; 7 | // $yellow: 8 | // $green: 9 | $teal: #04ace0; 10 | $cyan: #28c3f3; 11 | $white: #ffffff; 12 | $gray-400: #e6e6e6; 13 | $gray-600: #bbbbbb; 14 | $gray-800: #828282; 15 | 16 | $primary: $orange; 17 | $secondary: #bbbbbb; //custom 18 | $success: #10dc60; 19 | $info: $cyan; //custom 20 | $warning: #ffce00; 21 | $danger: #f53d3d; 22 | $light: #f4f4f4; 23 | $dark: #222222; 24 | 25 | $theme-colors: ( 26 | "squidink": #31465f, 27 | "bbb": #bbb, 28 | "lightsquid": #55677d, 29 | "ligthergray": #e6e6e6 30 | ); 31 | 32 | $font-family-sans-serif: Amazon Ember, -apple-system, BlinkMacSystemFont, 33 | "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", 34 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default; 35 | 36 | @import "bootstrap/scss/bootstrap"; 37 | 38 | html, 39 | body { 40 | height: 100%; 41 | } 42 | 43 | .scrollable { 44 | height: 250px; 45 | overflow-y: scroll; 46 | } 47 | 48 | .highlight img:hover { 49 | opacity: 0.5; 50 | background-color: #152939; 51 | border: 1px solid #152939; 52 | } 53 | 54 | .bg-ember { 55 | background-color: #ffac31 !important; 56 | } 57 | 58 | .bg-blue { 59 | background-color: #007eb9 !important; 60 | } 61 | 62 | .bg-ampligygray { 63 | background-color: #bbbbbb !important; 64 | } 65 | 66 | .text-squidink { 67 | color: #55677d !important; 68 | } 69 | 70 | .text-blue { 71 | color: #04ace0 !important; 72 | } 73 | 74 | @keyframes quiet { 75 | 25% { 76 | transform: scaleY(0.6); 77 | } 78 | 50% { 79 | transform: scaleY(0.4); 80 | } 81 | 75% { 82 | transform: scaleY(0.8); 83 | } 84 | } 85 | 86 | @keyframes normal { 87 | 25% { 88 | transform: scaleY(1); 89 | } 90 | 50% { 91 | transform: scaleY(0.4); 92 | } 93 | 75% { 94 | transform: scaleY(0.6); 95 | } 96 | } 97 | @keyframes loud { 98 | 25% { 99 | transform: scaleY(1); 100 | } 101 | 50% { 102 | transform: scaleY(0.4); 103 | } 104 | 75% { 105 | transform: scaleY(1.2); 106 | } 107 | } 108 | 109 | .boxContainer { 110 | display: flex; 111 | justify-content: space-between; 112 | height: 64px; 113 | --boxSize: 8px; 114 | --gutter: 4px; 115 | width: calc((var(--boxSize) + var(--gutter)) * 5); 116 | } 117 | 118 | .box { 119 | transform: scaleY(0.4); 120 | height: 100%; 121 | width: var(--boxSize); 122 | background: #ff9900; 123 | animation-duration: 1.2s; 124 | animation-timing-function: ease-in-out; 125 | animation-iteration-count: infinite; 126 | border-radius: 8px; 127 | } 128 | 129 | .box1 { 130 | animation-name: quiet; 131 | } 132 | 133 | .box2 { 134 | animation-name: normal; 135 | } 136 | 137 | .box3 { 138 | animation-name: quiet; 139 | } 140 | 141 | .box4 { 142 | animation-name: loud; 143 | } 144 | 145 | .box5 { 146 | animation-name: quiet; 147 | } 148 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------