├── .github ├── solutionid_validator.sh └── workflows │ └── maintainer_workflows.yml ├── .gitignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── assets └── arch.png ├── deployment ├── app.py ├── cdk.json ├── create-user.sh ├── infra.py ├── requirements.txt └── update-config.sh └── source ├── backend ├── lambda_fn.py └── requirements.txt ├── frontend ├── package-lock.json ├── package.json ├── public │ ├── Amazon-Ember-Medium.ttf │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── bedrock.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ ├── robots.txt │ └── site.webmanifest └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── Wrapper.js │ ├── bedrock.png │ ├── config.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ ├── routing.js │ └── setupTests.js └── webpack.config.js /.github/solutionid_validator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #set -e 3 | 4 | echo "checking solution id $1" 5 | echo "grep -nr --exclude-dir='.github' "$1" ./.." 6 | result=$(grep -nr --exclude-dir='.github' "$1" ./..) 7 | if [ $? -eq 0 ] 8 | then 9 | echo "Solution ID $1 found\n" 10 | echo "$result" 11 | exit 0 12 | else 13 | echo "Solution ID $1 not found" 14 | exit 1 15 | fi 16 | 17 | export result 18 | -------------------------------------------------------------------------------- /.github/workflows/maintainer_workflows.yml: -------------------------------------------------------------------------------- 1 | # Workflows managed by aws-solutions-library-samples maintainers 2 | name: Maintainer Workflows 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the "main" branch 5 | push: 6 | branches: [ "main" ] 7 | pull_request: 8 | branches: [ "main" ] 9 | types: [opened, reopened, edited] 10 | 11 | jobs: 12 | CheckSolutionId: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Run solutionid validator 17 | run: | 18 | chmod u+x ./.github/solutionid_validator.sh 19 | ./.github/solutionid_validator.sh ${{ vars.SOLUTIONID }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### vsCode ### 2 | .vscode 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | # Files that might appear in the root of a volume 10 | .DocumentRevisions-V100 11 | .fseventsd 12 | .Spotlight-V100 13 | .TemporaryItems 14 | .Trashes 15 | .VolumeIcon.icns 16 | .com.apple.timemachine.donotpresent 17 | 18 | **/node_modules 19 | **/dist 20 | **/build 21 | **/.DS_Store 22 | **/.angular 23 | **/.idea 24 | cdk.out 25 | **/data/**.csv 26 | 27 | .env 28 | 29 | # Python 30 | __pycache__/ 31 | *.py[cod] -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | CODEOWNERS @aws-solutions-library-samples/maintainers 2 | /.github/workflows/maintainer_workflows.yml @aws-solutions-library-samples/maintainers 3 | /.github/solutionid_validator.sh @aws-solutions-library-samples/maintainers 4 | -------------------------------------------------------------------------------- /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, or recently closed, 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 *main* 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' 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](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | This project includes the following third-party software/licensing: 2 | 3 | ** aws-amplify - https://github.com/aws-amplify/amplify-js | Apache-2.0 4 | 5 | ** react - https://github.com/facebook/react | MIT 6 | 7 | ** react-router - https://github.com/remix-run/react-router | MIT 8 | 9 | ** web-vitals - https://github.com/GoogleChrome/web-vitals | Apache-2.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generating Product Descriptions with Bedrock 2 | 3 | This solution contains a serverless backend and ReactJS front-end application which creates product descriptions from images and text input, enhances and translates product descriptions using the new managed generative AI service, Amazon Bedrock. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Overview](#overview) 8 | - [Cost](#cost) 9 | 2. [Prerequisites](#prerequisites) 10 | 3. [Deployment Steps](#deployment-steps) 11 | 4. [Deployment Validation](#deployment-validation) 12 | 5. [Running the Guidance](#running-the-guidance) 13 | 6. [Next Steps](#next-steps) 14 | 7. [Cleanup](#cleanup) 15 | 8. [Additional considerations and limitations](#additional-considerations-and-limitations) 16 | 17 | 18 | ## Overview 19 | 20 | Retail businesses often have many thousands or millions of products, all of which require accurate and effective descriptions. Retailers often have existing metadata or images for these products that can be provided as inputs to generative AI models to greatly accelerate the process of creating product descriptions. 21 | 22 | ![architecture diagram](assets/arch.png) 23 | 24 | 1. The client sends a request including input data (either a product image or a basic product description to enhance) to the Amazon API Gateway REST API. 25 | 2. Amazon API Gateway passes the request to AWS Lambda through a proxy integration. 26 | 3. When operating on product image inputs, AWS Lambda calls Amazon Rekognition, which uses machine learning to detect objects in the image. Descriptions of the objects detected can then be fed into subsequent steps to create a product description. 27 | 4. AWS Lambda calls large language models (LLMs) hosted by Amazon Bedrock, such as the Amazon Titan language models, to produce product descriptions. The LLMs can either enhance existing basic text descriptions, or generate new descriptions based on the objects detected by Amazon Rekognition. 28 | 5. AWS Lambda passes the response to Amazon API Gateway. 29 | 6. Finally, Amazon API Gateway returns the HTTP response to the client. 30 | 31 | 32 | ### Cost 33 | 34 | _You are responsible for the cost of the AWS services used while running this Guidance. As of November 2023, the cost for running this Guidance with the default settings in the US East (N. Virginia) is approximately $86 per month for processing 1000 products each month._ 35 | 36 | This guidance includes the AWS services Rekognition, Bedrock, Cognito, Lambda, API Gateway and CloudWatch with costs as follow: 37 | 38 | - Rekognition: $0.001 per image (for the first million images) 39 | - Bedrock: Depends on the model. Claude Instant for example is $0.00163 per 1000 input tokens and $0.00551 per 1000 output tokens, and the AI21 Jurassic model is $0.0125 per 1000 input/output tokens 40 | - Cognito: Free for up to 50k users ($0.0055 per monthly active user thereafter) 41 | - API Gateway: $3.50 per million requests 42 | - Lambda: $0.0000166667 per GB second or $0.0005 per invocation for this guidance assuming a worst-case 30s duration 43 | - CloudWatch logs: $0.50 per GB collected, $0.03 per month per GB stored 44 | 45 | Conservatively (erring on the side of overestimating), using this solution to generate product descriptions based on input images 1000 times per month (including generating translations into Spanish, German, and French), and assuming a worst case of 30s per Lambda function invocation and 1000 input/output tokens for all calls to Claude Instant and Jurassic models, costs would be approximately as follows: 46 | 47 | - Rekognition: $0.001 per image * 1000 images = $1.00 48 | - Bedrock: $0.00163 + $0.00551 per call to Claude Instant * 1000 calls = $7.14 and $0.0125 * 2 per call to Jurassic * 1000 calls * 3 languages = $75 49 | - Cognito: Free 50 | - API Gateway: $3.50 per million invocations * 5000 total invocations = $0.0175 51 | - Lambda: $0.0005 per invocation * 5000 total invocations = $2.50 52 | - CloudWatch logs: Conservatively assume 1 GB collected, costing approximately $0.53 for the first month 53 | - TOTAL: $86 per month with 1000 uses per month (or only $11 per month if translations are disabled) 54 | 55 | 56 | ## Prerequisites 57 | 58 | You'll need to install all prerequisites on your local machine: 59 | 60 | 1. [Python 3.8 or higher](https://www.python.org/downloads/macos/) 61 | 2. [Node.js & npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 62 | 3. Install the AWS CDK Toolkit (the `cdk` command) as documented [here](https://docs.aws.amazon.com/cdk/v2/guide/cli.html). You will also need to run `cdk bootstrap` if you haven't used the CDK before in your account as discussed [here](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html). 63 | 4. The [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) 64 | 5. [Docker](https://www.docker.com/) is required to allow the CDK to package the Lambda code together with the necessary Python dependencies 65 | 6. The project should work in any macOS, Linux, or Windows environment with the above prerequisites, but the project has been tested in macOS and Linux. Some commands and utility scripts assume a Bash shell environment. 66 | 7. The Claude Instant, Stable Diffusion SDXL0.8, and AI21 Jurassic-2 Mid must be enabled in Bedrock as documented [here](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) 67 | 68 | **NOTE:** Docker must be installed and _running_. You can ensure that the Docker daemon is running by ensuring that a command like `docker ps` runs without error. If no containers are running, then `docker ps` should return an empty list of containers like this: 69 | 70 | ``` 71 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 72 | ``` 73 | 74 | ## Deployment Steps 75 | 76 | 1. Create an [EC2 environment in AWS Cloud9](https://docs.aws.amazon.com/cloud9/latest/user-guide/create-environment-main.html), launch the EC2 instance into a public subnet, and write down its Public IPv4 address (#cloud9_ec2_ip). 77 | 78 | 2. [Resize the EBS volume](https://docs.aws.amazon.com/cloud9/latest/user-guide/move-environment.html#move-environment-resize) that the environment (created in Step 1) uses to at least 20GB. It comes with 10GB for t2.micro by default. 79 | 80 | 3. Clone the repo: 81 | ``` 82 | git clone https://github.com/aws-solutions-library-samples/guidance-for-generating-product-descriptions-with-bedrock.git 83 | ``` 84 | 4. cd to the repo folder: 85 | ``` 86 | cd guidance-for-generating-product-descriptions-with-bedrock 87 | ``` 88 | 5. (Optional) create a new Python virtualenv for project-specific dependencies: 89 | ``` 90 | python -m venv .env && source .env/bin/activate 91 | ``` 92 | 6. Install CDK dependencies: 93 | ``` 94 | pip install -r deployment/requirements.txt 95 | ``` 96 | 7. [Bootstrapping](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) for AWS CDK, if it has not been done previously: 97 | ``` 98 | cd deployment && cdk bootstrap 99 | ``` 100 | 8. Deploy the backend: 101 | ``` 102 | cd deployment && cdk deploy 103 | ``` 104 | 9. cd back to the project root: 105 | ``` 106 | cd .. 107 | ``` 108 | 10. Create an initial Cognito user: 109 | ``` 110 | deployment/create-user.sh <> 111 | ``` 112 | 11. Update `config.js` with the appropriate values from CDK stack outputs. This can be done automatically by running 113 | ``` 114 | deployment/update-config.sh 115 | ``` 116 | 12. Install frontend dependencies: 117 | ``` 118 | cd source/frontend && npm install 119 | ``` 120 | 13. Run the sample client app, and write down the port number (#web_port) that the webpack listens to, ex. 8080. 121 | ``` 122 | npm start 123 | ``` 124 | 14. Open the Security Groups of the EC2 created in Step 1, add an inbound rule, which allows Custom TCP, Port range #web_port, Source “My IP”, then Save rules. 125 | 126 | 127 | ## Deployment Validation 128 | 129 | The deployment should be successful if all of the above commands complete without error. You can browse the backend resources created by navigating to the CloudFormation service in the AWS Console, finding the stack named `LambdaStack`, and browsing its resources. 130 | 131 | ## Running the Guidance 132 | 133 | You can try the demo web app by following these steps: 134 | 1. Check your email for a temporary password 135 | 2. Access the app from a local browser at #cloud9_public_ip:#web_port 136 | 3. Follow the prompts to log in and change your password 137 | 4. To create a product description from an image (the default mode), browse for an image file for some product or object 138 | 5. Once uploaded, a product description and translations into several languages will be generated 139 | 140 | ## Next Steps 141 | 142 | See the Bedrock [product page](https://aws.amazon.com/bedrock/) for more resources on using Amazon Bedrock. 143 | 144 | 145 | ## Cleanup 146 | 147 | The provisioned infrastructure can be deleted by running the following command: 148 | ``` 149 | cd deployment && cdk destroy 150 | ``` 151 | 152 | 153 | **Additional considerations and limitations** 154 | 155 | - Because this project passes base64-encoded images into Lambda functions through the event payload, the total input size is limited to 6 MB, which limits the size of input images. 156 | - The Lambda and API Gateway proxy integration limits processing time to 29 seconds per call, which could prevent larger product descriptions from being generated. This limitation could be overcome using alternative approaches, such as streaming responses via Lambda function URLs instead of API Gateway. 157 | 158 | -------------------------------------------------------------------------------- /assets/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-generating-product-descriptions-with-amazon-bedrock/de6e7304f780181096d8b50bd14d39eb7891d081/assets/arch.png -------------------------------------------------------------------------------- /deployment/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | import aws_cdk as cdk 5 | 6 | from infra import LambdaStack 7 | 8 | 9 | app = cdk.App() 10 | LambdaStack(app, "LambdaStack", 11 | description="Generating Product Descriptions with Bedrock (SO9325)") 12 | app.synth() 13 | -------------------------------------------------------------------------------- /deployment/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "requirements*.txt", 11 | "source.bat", 12 | "**/__init__.py", 13 | "python/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": [ 21 | "aws", 22 | "aws-cn" 23 | ], 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 29 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 30 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 31 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 32 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 33 | "@aws-cdk/core:enablePartitionLiterals": true, 34 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 35 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 36 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 37 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 38 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 39 | "@aws-cdk/aws-route53-patters:useCertificate": true, 40 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 41 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 42 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 43 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 44 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 45 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 46 | "@aws-cdk/aws-redshift:columnId": true, 47 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 48 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 49 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 50 | "@aws-cdk/aws-kms:aliasNameRef": true, 51 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 52 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 53 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /deployment/create-user.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | email_address="$1" 4 | 5 | if [ -z $email_address ]; then 6 | echo "Email address must be provided as an argument to this script" 7 | echo "Example:" 8 | echo " deployment/create-user.sh foo@example.com" 9 | exit 1 10 | fi 11 | 12 | user_pool_id="$(aws cloudformation describe-stacks \ 13 | --stack-name LambdaStack \ 14 | --query "Stacks[0].Outputs[?OutputKey==\`CognitoUserPoolId\`].OutputValue" \ 15 | --output text)" 16 | 17 | aws cognito-idp admin-create-user \ 18 | --user-pool-id $user_pool_id \ 19 | --username $email_address \ 20 | --user-attributes Name="email",Value="$email_address" -------------------------------------------------------------------------------- /deployment/infra.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | BundlingOptions, 3 | Duration, 4 | CfnOutput, 5 | Stack, 6 | aws_apigateway as _apigw, 7 | aws_iam as iam, 8 | aws_lambda as _lambda, 9 | aws_logs as logs, 10 | aws_cognito as _cognito, 11 | Aspects 12 | ) 13 | from constructs import Construct 14 | import cdk_nag 15 | 16 | 17 | class LambdaStack(Stack): 18 | 19 | def __init__(self, scope: Construct, construct_id: str, **kwargs): 20 | super().__init__(scope, construct_id, **kwargs) 21 | 22 | ## userpool for api access 23 | api_userpool = _cognito.UserPool( 24 | self, "APIPool", 25 | advanced_security_mode=_cognito.AdvancedSecurityMode.ENFORCED, 26 | password_policy=_cognito.PasswordPolicy( 27 | min_length=8, 28 | require_lowercase=True, 29 | require_digits=True, 30 | require_symbols=True, 31 | require_uppercase=True 32 | ) 33 | ) 34 | 35 | ## api pool client for registration 36 | api_pool_client = api_userpool.add_client("app-client") 37 | 38 | ## authentication authorizer for apiGW 39 | api_auth = _apigw.CognitoUserPoolsAuthorizer( 40 | self, 'api_authorizer', 41 | cognito_user_pools=[api_userpool] 42 | ) 43 | 44 | ## output user pool ID for user sign up 45 | CfnOutput( 46 | self, 'CognitoUserPoolId', 47 | value=api_userpool.user_pool_id 48 | ) 49 | 50 | ## output client ID for user sign up 51 | CfnOutput( 52 | self, 'CognitoClientId', 53 | value=api_pool_client.user_pool_client_id 54 | ) 55 | 56 | fn_policy_doc = iam.PolicyDocument( 57 | statements=[ 58 | iam.PolicyStatement( 59 | effect=iam.Effect.ALLOW, 60 | actions=["bedrock:InvokeModel"], 61 | resources=[f"arn:{self.partition}:bedrock:{self.region}::foundation-model/*"]), 62 | iam.PolicyStatement( 63 | effect=iam.Effect.ALLOW, 64 | actions=["rekognition:DetectLabels"], 65 | # No specific resources for DetectLabels 66 | resources=["*"]), 67 | iam.PolicyStatement( 68 | effect=iam.Effect.ALLOW, 69 | actions=[ 70 | "logs:CreateLogGroup", 71 | "logs:CreateLogStream", 72 | "logs:PutLogEvents", 73 | ], 74 | resources=[f"arn:aws:logs:{self.region}:{self.account}:log-group:/aws/lambda/LambdaStack-*"]) 75 | ] 76 | ) 77 | 78 | lambda_role = iam.Role(self, "fn-role", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), 79 | inline_policies={"fn-policy": fn_policy_doc}) 80 | 81 | # Lambda 82 | fn = _lambda.Function(self, "Function", 83 | code=_lambda.Code.from_asset(f"{__file__}/../..", exclude=["cdk.out"], 84 | bundling=BundlingOptions( 85 | image=_lambda.Runtime.PYTHON_3_11.bundling_image, 86 | command=["bash", "-c", "pip install -r source/backend/requirements.txt -t /asset-output && cp source/backend/lambda_fn.py /asset-output"], 87 | ) 88 | ), 89 | runtime=_lambda.Runtime.PYTHON_3_11, 90 | handler="lambda_fn.handler", 91 | architecture=_lambda.Architecture.X86_64, 92 | timeout=Duration.seconds(60), 93 | memory_size=1024, 94 | role=lambda_role 95 | ) 96 | 97 | ## base API for post requests from front end 98 | front_end_api = _apigw.RestApi( 99 | self, 'bedrock_lambda_api', 100 | rest_api_name='bedrock_lambda_api' 101 | ) 102 | CfnOutput(self, "ApiUrl", value=front_end_api.url) 103 | 104 | api_validator = _apigw.RequestValidator( 105 | self, "feValidator", 106 | rest_api=front_end_api, 107 | validate_request_body=True, 108 | validate_request_parameters=True 109 | ) 110 | 111 | ## this adds /bedrock onto the api and enables cors 112 | api_resource = front_end_api.root.add_resource( 113 | 'bedrock', 114 | default_cors_preflight_options=_apigw.CorsOptions( 115 | allow_methods=['GET', 'OPTIONS', 'POST'], 116 | allow_origins=_apigw.Cors.ALL_ORIGINS) 117 | ) 118 | 119 | ## this makes the resource integrate with our _lambda 120 | api_lambda_integration = _apigw.LambdaIntegration( 121 | fn, 122 | proxy=False, 123 | integration_responses=[ 124 | _apigw.IntegrationResponse( 125 | status_code="200", 126 | response_parameters={ 127 | 'method.response.header.Access-Control-Allow-Origin': "'*'" 128 | } 129 | ) 130 | ] 131 | ) 132 | 133 | ## this adds a POST method on our resource 134 | api_resource.add_method( 135 | 'POST', api_lambda_integration, 136 | method_responses=[ 137 | _apigw.MethodResponse( 138 | status_code="200", 139 | response_parameters={ 140 | 'method.response.header.Access-Control-Allow-Origin': True 141 | } 142 | ) 143 | ], 144 | authorizer=api_auth, 145 | authorization_type=_apigw.AuthorizationType.COGNITO 146 | ) 147 | 148 | 149 | _NagSuppressions = cdk_nag.NagSuppressions() 150 | 151 | # Add Nag Suppressions here 152 | 153 | _NagSuppressions.add_resource_suppressions( 154 | lambda_role, 155 | suppressions=[ 156 | { 157 | "id": "AwsSolutions-IAM5", 158 | "reason": "The wildcard exists to allow the lambda function to invoke any foundational model in Bedrock. Users must explicitly enable access to all foundational models in the Amazon Bedrock Web Console first." 159 | } 160 | ], 161 | apply_to_children=True 162 | ) 163 | 164 | _NagSuppressions.add_resource_suppressions( 165 | fn, 166 | suppressions=[ 167 | { 168 | "id": "AwsSolutions-L1", 169 | "reason": "The Lambda function is using the latest available runtime for the targeted language." 170 | } 171 | ], 172 | apply_to_children=True 173 | ) 174 | 175 | _NagSuppressions.add_resource_suppressions( 176 | front_end_api, 177 | suppressions=[ 178 | { 179 | "id": "AwsSolutions-APIG1", 180 | "reason": "All remaining methods which do not have logging enabled are OPTIONS methods." 181 | }, 182 | { 183 | "id": "AwsSolutions-APIG4", 184 | "reason": "All remaining methods which do not have logging enabled are OPTIONS methods." 185 | }, 186 | { 187 | "id": "AwsSolutions-COG4", 188 | "reason": "All remaining methods which do not have logging enabled are OPTIONS methods." 189 | }, 190 | { 191 | "id": "AwsSolutions-APIG6", 192 | "reason": "All remaining methods which do not have logging enabled are OPTIONS methods." 193 | } 194 | ], 195 | apply_to_children=True 196 | ) 197 | 198 | Aspects.of(self).add(cdk_nag.AwsSolutionsChecks(verbose=True)) 199 | 200 | -------------------------------------------------------------------------------- /deployment/requirements.txt: -------------------------------------------------------------------------------- 1 | cdk-nag==2.27.181 2 | aws-cdk-lib==2.104.0 -------------------------------------------------------------------------------- /deployment/update-config.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | api_url="$(aws cloudformation describe-stacks \ 4 | --stack-name LambdaStack \ 5 | --query "Stacks[0].Outputs[?OutputKey==\`ApiUrl\`].OutputValue" \ 6 | --output text)" 7 | client_id="$(aws cloudformation describe-stacks \ 8 | --stack-name LambdaStack \ 9 | --query "Stacks[0].Outputs[?OutputKey==\`CognitoClientId\`].OutputValue" \ 10 | --output text)" 11 | user_pool_id="$(aws cloudformation describe-stacks \ 12 | --stack-name LambdaStack \ 13 | --query "Stacks[0].Outputs[?OutputKey==\`CognitoUserPoolId\`].OutputValue" \ 14 | --output text)" 15 | 16 | region="$(aws configure get region)" 17 | 18 | root="$(git rev-parse --show-toplevel)" 19 | cat <0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "@testing-library/jest-dom": "^5.16.5", 40 | "@testing-library/react": "^13.4.0", 41 | "@testing-library/user-event": "^13.5.0", 42 | "http-proxy-middleware": "^2.0.6", 43 | "react-scripts": "5.0.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /source/frontend/public/Amazon-Ember-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-generating-product-descriptions-with-amazon-bedrock/de6e7304f780181096d8b50bd14d39eb7891d081/source/frontend/public/Amazon-Ember-Medium.ttf -------------------------------------------------------------------------------- /source/frontend/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-generating-product-descriptions-with-amazon-bedrock/de6e7304f780181096d8b50bd14d39eb7891d081/source/frontend/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /source/frontend/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-generating-product-descriptions-with-amazon-bedrock/de6e7304f780181096d8b50bd14d39eb7891d081/source/frontend/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /source/frontend/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-generating-product-descriptions-with-amazon-bedrock/de6e7304f780181096d8b50bd14d39eb7891d081/source/frontend/public/apple-touch-icon.png -------------------------------------------------------------------------------- /source/frontend/public/bedrock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-generating-product-descriptions-with-amazon-bedrock/de6e7304f780181096d8b50bd14d39eb7891d081/source/frontend/public/bedrock.png -------------------------------------------------------------------------------- /source/frontend/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-generating-product-descriptions-with-amazon-bedrock/de6e7304f780181096d8b50bd14d39eb7891d081/source/frontend/public/favicon-16x16.png -------------------------------------------------------------------------------- /source/frontend/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-generating-product-descriptions-with-amazon-bedrock/de6e7304f780181096d8b50bd14d39eb7891d081/source/frontend/public/favicon-32x32.png -------------------------------------------------------------------------------- /source/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-generating-product-descriptions-with-amazon-bedrock/de6e7304f780181096d8b50bd14d39eb7891d081/source/frontend/public/favicon.ico -------------------------------------------------------------------------------- /source/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Amazon Bedrock App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /source/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo512.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /source/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /source/frontend/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /source/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Ember; 3 | src: local('Amazon-Ember.Medium'), url(../public/Amazon-Ember-Medium.ttf) 4 | } 5 | 6 | * { 7 | font-family: Ember!important; 8 | } 9 | 10 | .App { 11 | text-align: center; 12 | } 13 | 14 | .CrawlModal { 15 | height: 100vh; 16 | width: 100vw; 17 | position: fixed; 18 | background-color: rgba(0, 0, 0, 0.402); 19 | top: 0px; 20 | left: 0; 21 | flex-direction: row; 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | 26 | 27 | 28 | .UberLogo { 29 | height: 50px!important; 30 | margin-left: 120px; 31 | } 32 | 33 | .CrawlPrompt { 34 | height: 60vh; 35 | width: 60vw; 36 | background:rgb(255, 255, 255); 37 | -webkit-box-shadow: 3px 5px 15px -8px rgba(0,0,0,0.75); 38 | -moz-box-shadow: 3px 5px 15px -8px rgba(0,0,0,0.75); 39 | box-shadow: 2px 2px 15px 2px rgba(0,0,0,0.25); 40 | display: flex; 41 | flex-direction: column; 42 | justify-content: flex-start; 43 | } 44 | 45 | .chat-messages-crawl { 46 | height: 90%; 47 | width: 100%; 48 | overflow-y: scroll; 49 | 50 | } 51 | 52 | .MessageText { 53 | width: 80%!important; 54 | height: auto; 55 | border-radius: 3px; 56 | padding-left:5px; 57 | padding: 7px 7px; 58 | } 59 | 60 | .OtherModels div { 61 | padding: 0; 62 | margin:0; 63 | height: 60px; 64 | } 65 | 66 | .Bot-message .MessageText { 67 | background: #36dca4; 68 | width: auto; 69 | } 70 | 71 | .You-message .MessageText { 72 | border: 1px solid rgba(0, 0, 0, 0.102); 73 | } 74 | .MessageText, .MessageText #text { 75 | width: auto; 76 | max-width: 800px; 77 | text-align: left!important; 78 | } 79 | 80 | .MessageText ul, .MessageText ol { 81 | margin: 0px!important; 82 | padding-left: 0px; 83 | } 84 | 85 | .MessageText li { 86 | margin-left: 35px; 87 | } 88 | 89 | .Bot-message { 90 | 91 | width: auto; 92 | max-width: 800px; 93 | margin: 3px 0; 94 | padding: 10px 5px 10px 5px !important; 95 | border-radius: 4px; 96 | } 97 | 98 | .You-message { 99 | background: white; 100 | width: auto; 101 | max-width: auto; 102 | margin: 3px 0; 103 | padding: 10px 5px 10px 5px !important; 104 | border-radius: 4px; 105 | } 106 | 107 | .Bot-title { 108 | width: 20% !important; 109 | background:white; 110 | height: 100%; 111 | padding: 7px 0px; 112 | } 113 | 114 | .You-title { 115 | width: 20% !important; 116 | padding: 7px 0px; 117 | } 118 | 119 | .CrawlIndexAction { 120 | width: 100%; 121 | height: 80px; 122 | display: flex; 123 | flex-direction: row; 124 | justify-content: space-evenly; 125 | } 126 | 127 | .CrawlIndexAction button { 128 | width: 180px; 129 | height: 60px; 130 | 131 | } 132 | 133 | .CrawlPrompt ol li { 134 | padding-top: 5px; 135 | } 136 | 137 | 138 | .CrawlIndexAction button:hover { 139 | background: #36dca4; 140 | cursor: pointer; 141 | } 142 | 143 | .InfoModal { 144 | width: 600px; 145 | background: white; 146 | position:fixed; 147 | z-index:2000!important; 148 | top:60px; 149 | display: flex; 150 | flex-direction: row; 151 | justify-content: flex-end; 152 | -webkit-box-shadow: 3px 5px 15px -8px rgba(0,0,0,0.75); 153 | -moz-box-shadow: 3px 5px 15px -8px rgba(0,0,0,0.75); 154 | box-shadow: 3px 5px 15px -8px rgba(0,0,0,0.75); 155 | padding: 0px; 156 | margin: 0px; 157 | } 158 | 159 | .InnerInfoModal { 160 | padding: 20px; 161 | padding-right:20px; 162 | width: 96%; 163 | text-align: left; 164 | display: flex; 165 | flex-direction: column; 166 | justify-content: space-evenly; 167 | height: 100%; 168 | } 169 | 170 | .InnerInfoModal h1 { 171 | margin-top: 2px; 172 | margin-bottom:2px; 173 | font-size:26px; 174 | } 175 | 176 | .InnerInfoModal h2 { 177 | margin: 0; 178 | } 179 | 180 | .InfoModalButton { 181 | display: flex; 182 | flex-direction: column; 183 | justify-content: center; 184 | height: 100%; 185 | width: 40px; 186 | cursor: pointer; 187 | justify-self: right; 188 | } 189 | 190 | .InfoModalButton:hover { 191 | background:rgba(0, 0, 0, 0.102); 192 | } 193 | 194 | .ModalButton { 195 | display: flex; 196 | flex-direction: column; 197 | justify-content: center; 198 | height: 40px; 199 | width: 100%;; 200 | cursor: pointer; 201 | justify-self: right; 202 | } 203 | 204 | .ModalButton:hover { 205 | background:rgba(0, 0, 0, 0.102); 206 | } 207 | 208 | .InfoModalButton svg { 209 | fill: black; 210 | height: 30px; 211 | position: relative; 212 | left: 0px; 213 | } 214 | 215 | .closeModal { 216 | height: 20px; 217 | } 218 | 219 | #imageResponse { 220 | height: 80%!important; 221 | /* width: 100%; */ 222 | padding-top: 20px; 223 | } 224 | 225 | #imageResponse img { 226 | height: 80%!important; 227 | /* width: 100%; */ 228 | } 229 | 230 | 231 | .chat-container { 232 | display: flex; 233 | flex-direction: column; 234 | justify-content: flex-start; 235 | width: 80vw; 236 | align-self: center; 237 | height: 86vh; 238 | background: white; 239 | padding-top: 40px; 240 | padding-right: 40px; 241 | padding-left: 40px; 242 | padding-bottom: 40px; 243 | overflow-y: auto; 244 | } 245 | 246 | .chat-container p { 247 | padding-top: 5px!important; 248 | width: 220px; 249 | margin: 3px 0; 250 | display: flex; 251 | flex-direction: row; 252 | } 253 | 254 | 255 | 256 | .user-input-crawl { 257 | justify-self: flex-end!important; 258 | display: flex; 259 | flex-direction: row; 260 | height: 600px; 261 | margin-top:20px; 262 | border: 1px solid rgba(0, 0, 0, 0.202); 263 | position: rleative; 264 | bottom: 40px; 265 | width: 100%; 266 | /* margin-left: -40px; */ 267 | /* left: 30%; */ 268 | border-top: 3px solid rgba(0, 0, 0, 0.202); 269 | } 270 | 271 | .user-input-crawl textarea { 272 | width: 70%; 273 | border: none; 274 | /* border-bottom: 1px solid rgba(0, 0, 0, 0.202); 275 | border-top: 1px solid rgba(0, 0, 0, 0.202); 276 | border-left: 1px solid rgba(0, 0, 0, 0.202); */ 277 | /* padding-top: 0; */ 278 | padding-left: 10px; 279 | padding-top: 10px; 280 | resize: none; 281 | } 282 | 283 | .user-input { 284 | justify-self: flex-end!important; 285 | display: flex; 286 | flex-direction: row; 287 | height: 80px; 288 | margin-top:20px; 289 | border: 1px solid rgba(0, 0, 0, 0.202); 290 | position: absolute; 291 | bottom: 40px; 292 | width: 80vw; 293 | margin-left: -40px; 294 | /* left: 30%; */ 295 | border-top: 3px solid rgba(0, 0, 0, 0.202); 296 | } 297 | 298 | .user-input textarea { 299 | width: 70%; 300 | border: none; 301 | /* border-bottom: 1px solid rgba(0, 0, 0, 0.202); 302 | border-top: 1px solid rgba(0, 0, 0, 0.202); 303 | border-left: 1px solid rgba(0, 0, 0, 0.202); */ 304 | /* padding-top: 0; */ 305 | padding-left: 10px; 306 | padding-top: 10px; 307 | resize: none; 308 | } 309 | .user-input-product { 310 | justify-self: flex-end!important; 311 | display: flex; 312 | flex-direction: row; 313 | height: 120px; 314 | margin-top:20px; 315 | border: 1px solid rgba(0, 0, 0, 0.202); 316 | /* position: absolute; 317 | bottom: 40px; */ 318 | width: 100%; 319 | /* margin-left: -40px; */ 320 | /* left: 30%; */ 321 | border-top: 3px solid rgba(0, 0, 0, 0.202); 322 | } 323 | 324 | .user-input-product textarea { 325 | width: 70%; 326 | border: none; 327 | /* border-bottom: 1px solid rgba(0, 0, 0, 0.202); 328 | border-top: 1px solid rgba(0, 0, 0, 0.202); 329 | border-left: 1px solid rgba(0, 0, 0, 0.202); */ 330 | /* padding-top: 0; */ 331 | /* padding-left: 10px; 332 | padding-top: 10px; */ 333 | resize: none; 334 | } 335 | 336 | .airwolf-header svg { 337 | fill: white; 338 | height: 35px!important; 339 | margin-top: 12px; 340 | position: absolute; 341 | left: 215px; 342 | } 343 | 344 | 345 | .airwolf-header svg path { 346 | fill: white!important; 347 | height: 40px!important; 348 | margin-top: 10px; 349 | } 350 | 351 | 352 | .user-input button { 353 | width: 30%; 354 | border: none; 355 | cursor: pointer; 356 | } 357 | 358 | .user-input-product button { 359 | width: 30%; 360 | border: none; 361 | cursor: pointer; 362 | } 363 | 364 | .user-input-crawl button { 365 | width: 30%; 366 | border: none; 367 | cursor: pointer; 368 | } 369 | 370 | .ClearHistory { 371 | width: 120px!important; 372 | height: auto; 373 | /* position: absolute; */ 374 | bottom: -25px; 375 | cursor: pointer; 376 | /* left: 40%; */ 377 | /* top: 80px; 378 | z-index:999999999999!important; */ 379 | border-right: 4px solid rgba(0, 0, 0, 0.202)!important; 380 | } 381 | 382 | .CrawlReindex { 383 | width: 60px!important; 384 | height: 60px; 385 | position: fixed; 386 | bottom: -25px; 387 | cursor: pointer; 388 | /* left: 40%; */ 389 | bottom: 20px; 390 | left: calc(100% - 80px); 391 | z-index:40!important; 392 | fill:#1c7c5c; 393 | display: flex; 394 | flex-direction: column; 395 | justify-content: center; 396 | align-items: center; 397 | 398 | background:rgb(255, 255, 255); 399 | /* border-right: 4px solid rgba(0, 0, 0, 0.202)!important; */ 400 | } 401 | 402 | .CrawlReindex svg { 403 | width: 40px!important; 404 | height: 40px; 405 | 406 | /* border-right: 4px solid rgba(0, 0, 0, 0.202)!important; */ 407 | } 408 | 409 | .user-input button:hover, .user-input-product button:hover, .user-input-crawl button:hover { 410 | background: #FF9900; 411 | } 412 | 413 | .user-input button:disabled { 414 | background: rgb(239,239,239)!important; 415 | cursor:wait; 416 | } 417 | 418 | .chat-container p span { 419 | width: 100; 420 | text-align: right; 421 | padding-right: 10px; 422 | } 423 | 424 | 425 | .App-logo { 426 | height: 40vmin; 427 | pointer-events: none; 428 | } 429 | 430 | @media (prefers-reduced-motion: no-preference) { 431 | .App-logo { 432 | animation: App-logo-spin infinite 20s linear; 433 | } 434 | } 435 | 436 | .App-header { 437 | background-color: #282c34; 438 | min-height: 100vh; 439 | display: flex; 440 | flex-direction: column; 441 | align-items: center; 442 | justify-content: center; 443 | font-size: calc(10px + 2vmin); 444 | color: white; 445 | } 446 | 447 | .App-link { 448 | color: #61dafb; 449 | } 450 | 451 | @keyframes App-logo-spin { 452 | from { 453 | transform: rotate(0deg); 454 | } 455 | to { 456 | transform: rotate(360deg); 457 | } 458 | } 459 | 460 | .App { 461 | width: 100vw; 462 | height: 100vh; 463 | } 464 | 465 | #UpperNav { 466 | height: 60px; 467 | width: 100vw; 468 | display: flex; 469 | top:0; 470 | flex-direction: row; 471 | justify-content: space-between; 472 | border-bottom: 1px solid black; 473 | -webkit-box-shadow: 0px 5px 15px -8px rgba(0,0,0,0.75); 474 | -moz-box-shadow: 0px 5px 15px -8px rgba(0,0,0,0.75); 475 | box-shadow: 0px 5px 15px -8px rgba(0,0,0,0.75); 476 | /* background: rgba(35, 47, 62, 1); */ 477 | color: white; 478 | position: fixed; 479 | top:0; 480 | /* font-size: 24px!important; */ 481 | 482 | z-index: 3!important; 483 | } 484 | 485 | #UpperNav * { 486 | align-items: center; 487 | vertical-align: middle; 488 | display: flex; 489 | border: none; 490 | } 491 | 492 | #UpperNav span { 493 | padding-left: 20px; 494 | font-size: 20px!important; 495 | font-weight: 600; 496 | letter-spacing: 1px; 497 | } 498 | 499 | #UpperNav button { 500 | color: white; 501 | border-radius: none!important; 502 | } 503 | 504 | #UpperNav button:hover { 505 | color: red; 506 | border-radius: none!important; 507 | } 508 | 509 | .CardholderInfo { 510 | display: flex; 511 | flex-direction: column; 512 | height: 110px; 513 | width: 400px; 514 | justify-content: space-evenly; 515 | } 516 | 517 | .CardholderInfo span { 518 | display: flex; 519 | flex-direction: row; 520 | justify-content: space-between; 521 | width: 100%; 522 | height: 40px; 523 | } 524 | 525 | .CardholderInfo label { 526 | display: flex; 527 | flex-direction: row; 528 | justify-content: space-between; 529 | width: 100%; 530 | height: 40px; 531 | vertical-align: middle; 532 | padding-top: 8px; 533 | } 534 | 535 | .CardholderInfo input { 536 | height: 40px; 537 | width: 700px; 538 | padding-left: 10px; 539 | } 540 | 541 | #cis1 { 542 | width: 150px; 543 | text-align: left; 544 | } 545 | 546 | .CustInfoSpan { 547 | display: flex; 548 | flex-direction: row; 549 | width: 100%; 550 | justify-content: flex-start; 551 | } 552 | 553 | #UpperNav h4, #ModalUpperNav h4 { 554 | padding-left: 20px; 555 | } 556 | 557 | .amplify-flex.amplify-tabs { 558 | display:none; 559 | } 560 | 561 | #ContentSection { 562 | padding: 40px; 563 | padding-top: 90px; 564 | padding-bottom: 0px; 565 | width: 100vw; 566 | height: 100vh!important; 567 | display: flex; 568 | flex-direction: row; 569 | justify-content: center; 570 | /* px; */ 571 | position: fixed; 572 | top:0; 573 | z-index: 10; 574 | overflow-y: scroll; 575 | padding: 90px 20px 0px 20px; 576 | background:white 577 | } 578 | 579 | .ProductDesc { 580 | width: 80vw; 581 | text-align: left; 582 | padding-top: 20px; 583 | font-size: 20px; 584 | font-weight: 600!important; 585 | } 586 | 587 | .Contents form { 588 | margin: 20px; 589 | } 590 | 591 | #imageUpload::file-selector-button { 592 | height: 40px; 593 | } 594 | 595 | .Contents { 596 | width: 80vw; 597 | height: auto; 598 | background: white; 599 | box-sizing: border-box; 600 | } 601 | 602 | .Translations { 603 | width: 100%; 604 | height: auto; 605 | display: flex; 606 | flex-direction: column; 607 | justify-content: flex-start; 608 | padding-top: 30px; 609 | } 610 | 611 | .ImageLabel { 612 | height: 500px; 613 | } 614 | 615 | #imageResponse, #imageResponse img { 616 | height: 500px!important; 617 | } 618 | 619 | .Demos { 620 | width: 100%; 621 | height: 70px; 622 | display: flex; 623 | flex-direction: column; 624 | justify-content: flex-start; 625 | /* padding-top: 10px; */ 626 | } 627 | 628 | .TranslationOptions, .DemoOptions { 629 | width: 100%; 630 | height: 60px; 631 | display: flex; 632 | flex-direction: row; 633 | justify-content: flex-start; 634 | /* border-top: 1px solid rgba(0, 0, 0, 0.302); 635 | border-bottom: 1px solid rgba(0, 0, 0, 0.302); */ 636 | } 637 | 638 | h1 { 639 | text-align: left; 640 | padding-left: 40px; 641 | } 642 | 643 | .TranslationOptions span, .DemoOptions span { 644 | width: 100%; 645 | height: 58px; 646 | display: inline-block; 647 | flex-direction: row; 648 | justify-content: center; 649 | align-content: middle; 650 | padding: 13px; 651 | font-weight: 600; 652 | padding-top: 17px; 653 | white-space: nowrap; 654 | } 655 | .TranslationOptions span.active, .DemoOptions span.active { 656 | background: rgba(255, 153, 0, 0.8)!important; 657 | } 658 | 659 | .TranslationOptions span:hover, .DemoOptions span:hover { 660 | background:rgba(0, 0, 0, 0.102); 661 | cursor: pointer; 662 | } 663 | 664 | 665 | 666 | .ProductDescription { 667 | padding: 120px; 668 | text-align: left; 669 | padding-top: 20px; 670 | font-size: 20px; 671 | } 672 | 673 | 674 | #ContentSection h2 { 675 | text-align: left; 676 | width: 600px; 677 | align-self: center; 678 | } 679 | 680 | #ContentSection p { 681 | text-align: left; 682 | width: 800px; 683 | align-self: center; 684 | padding-top: 20px; 685 | } 686 | 687 | .FormSection { 688 | width: 600px; 689 | align-self: center; 690 | 691 | } 692 | 693 | .FormSection form { 694 | display: flex; 695 | flex-direction: column; 696 | } 697 | 698 | .FormSection form span { 699 | display: flex; 700 | flex-direction: row; 701 | height: 60px; 702 | 703 | } 704 | 705 | span#devicesContainer { 706 | width: 100%; 707 | height: auto; 708 | padding-bottom: 20px; 709 | display: flex; 710 | flex-direction: column; 711 | } 712 | 713 | div.device { 714 | height: 50px; 715 | width: 100%; 716 | display:flex; 717 | flex-direction: column; 718 | justify-content: space-between; 719 | border: solid 1px black; 720 | overflow: hidden; 721 | -webkit-box-shadow: 2px 5px 15px 5px rgba(0,0,0,0.55); 722 | -moz-box-shadow: 2px 5px 15px 5px rgba(0,0,0,0.55); 723 | box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.45); 724 | 725 | } 726 | 727 | div.deviceNav, #DeviceNav { 728 | height: 50px!important; 729 | width: 100%; 730 | display: flex; 731 | flex-direction: row; 732 | justify-content: space-between; 733 | align-self: flex-start; 734 | } 735 | 736 | div.deviceLabel { 737 | height: 50px!important; 738 | width: 60%; 739 | text-align: left; 740 | padding-left: 10px; 741 | display: flex; 742 | flex-direction: row; 743 | align-items: center; 744 | } 745 | 746 | .deviceLabel svg { 747 | height: 35px; 748 | position: relative; 749 | } 750 | 751 | .deviceLabel span { 752 | position: relative; 753 | height: auto!important; 754 | } 755 | 756 | 757 | div.deviceStatus { 758 | height: 100%; 759 | width: auto; 760 | display: flex; 761 | flex-direction: row; 762 | align-items: center; 763 | } 764 | 765 | div.deviceStatus span { 766 | position: relative; 767 | font-size: 16px; 768 | padding-left: 15px; 769 | height: auto!important; 770 | padding-right: 15px; 771 | } 772 | 773 | 774 | .deviceDetails { 775 | height: 250px; 776 | width: 100%; 777 | display: flex; 778 | flex-direction: column; 779 | padding-left: 40px; 780 | padding-top: 20px; 781 | border-top: 1px solid black; 782 | } 783 | 784 | .deviceDetails button { 785 | margin-top: 15px; 786 | height: 50px; 787 | width: 150px; 788 | 789 | } 790 | 791 | .deviceDetail { 792 | text-align: left; 793 | height: 120; 794 | display: flex!important; 795 | flex-direction: column!important; 796 | } 797 | 798 | .deviceActionsContainer { 799 | width: 100%; 800 | display: flex; 801 | flex-direction: row; 802 | justify-content: space-around; 803 | color: #912a1e 804 | } 805 | 806 | .DeviceExpandButton { 807 | height: 50px; 808 | width: 50px; 809 | /* border: 0.1px solid rgba(0, 0, 0, 0.3); */ 810 | display: flex; 811 | flex-direction: column; 812 | justify-content: center; 813 | } 814 | 815 | .DeviceExpandButton:hover { 816 | fill: #3f9a45; 817 | cursor: pointer!important; 818 | } 819 | 820 | .deviceStatus svg { 821 | height: 25px; 822 | position: relative; 823 | cursor: pointer!important; 824 | } 825 | 826 | 827 | 828 | 829 | .FormSection label { 830 | padding-top: 7.5px; 831 | font-weight: 700; 832 | width: 140px; 833 | height: 60px; 834 | text-align: left; 835 | 836 | } 837 | 838 | select { 839 | width: 400px; 840 | margin-left: 10px; 841 | padding: 3px; 842 | padding-left:20px; 843 | height: 40px; 844 | appearance: none; 845 | background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); 846 | background-repeat: no-repeat; 847 | background-position: right 1rem center; 848 | background-size: 1em; 849 | cursor: pointer!important; 850 | } 851 | 852 | select option { 853 | text-transform: uppercase!important; 854 | } 855 | 856 | hr.amplify-divider { 857 | margin-top: 20px; 858 | border: none; 859 | } 860 | 861 | .LogButton { 862 | display: none; 863 | } 864 | 865 | .ModalContainer { 866 | position: fixed; 867 | top: 0px; 868 | left: 0px; 869 | height: 100vh; 870 | width: 100vw; 871 | flex-direction: row; 872 | vertical-align: middle; 873 | justify-content: center; 874 | } 875 | 876 | #Modal { 877 | position: fixed; 878 | top: 10vh; 879 | left: calc(50% - 400px); 880 | width: 800px; 881 | /* height: auto; */ 882 | overflow: hidden; 883 | background: white; 884 | -webkit-box-shadow: 0px 10px 38px -8px rgba(0,0,0,0.75); 885 | -moz-box-shadow: 0px 10px 38px -8px rgba(0,0,0,0.75); 886 | box-shadow: 0px 10px 38px -8px rgba(0,0,0,0.75); 887 | display:flex; 888 | flex-direction: column; 889 | border: 1px solid black; 890 | transition: opacity 0.3s linear, max-height 0.2s linear; 891 | box-sizing: border-box; 892 | } 893 | 894 | 895 | 896 | h4 { 897 | padding-top: 0; 898 | } 899 | 900 | .ModalForm { 901 | display: flex; 902 | flex-direction: column; 903 | align-items: flex-start; 904 | padding: 20px 80px 10px 60px; 905 | } 906 | 907 | .ModalButtons { 908 | width: 100%; 909 | display: flex; 910 | flex-direction: row; 911 | justify-content: flex-end; 912 | padding-right: 110px; 913 | justify-self: flex-end; 914 | align-self: flex-end; 915 | /* position: absolute; */ 916 | bottom: 80px; 917 | left: 0px; 918 | margin: 30px 0px; 919 | } 920 | 921 | .SubmitButton { 922 | width: 200px!important; 923 | height: 50px!important; 924 | /* position: relative!important; */ 925 | /* left: 180px!important; */ 926 | } 927 | 928 | .SubmitButton:hover { 929 | border: 1px solid green!important; 930 | 931 | } 932 | 933 | #ModalUpperNav { 934 | width: 100%; 935 | height: 50px; 936 | font-size: 18px; 937 | background: rgba(35, 47, 62, 1); 938 | position: relative; 939 | display: flex; 940 | flex-direction: row; 941 | justify-content: space-between; 942 | border-bottom: 1px solid black; 943 | -webkit-box-shadow: 0px 5px 15px -8px rgba(0,0,0,0.75); 944 | -moz-box-shadow: 0px 5px 15px -8px rgba(0,0,0,0.75); 945 | box-shadow: 0px 5px 15px -8px rgba(0,0,0,0.75); 946 | /* background: rgba(35, 47, 62, 1); */ 947 | color: white; 948 | /* background: */ 949 | } 950 | 951 | #Modal p { 952 | padding: 40px; 953 | font-size: 20px!important; 954 | align-self: center; 955 | } 956 | 957 | .amplify-button:hover, .amplify-field-group__control:hover { 958 | /* border: none!important; */ 959 | } 960 | 961 | form { 962 | padding-top: 30px; 963 | } 964 | 965 | .CardSelectionContainer { 966 | height: auto; 967 | display: flex; 968 | flex-direction: column; 969 | display: flex; 970 | align-items: flex-start; 971 | padding-left: 60px; 972 | padding-right: 60px; 973 | } 974 | 975 | .CardGraphic { 976 | height: 92px; 977 | width: 158px; 978 | border-radius: 7px; 979 | } 980 | 981 | .CardGraphic:hover { 982 | -webkit-box-shadow: 0px 5px 15px -8px rgba(0,0,0,0.75); 983 | -moz-box-shadow: 0px 5px 15px -8px rgba(0,0,0,0.75); 984 | box-shadow: 0px 5px 15px -8px rgba(0,0,0,0.75); 985 | cursor: pointer; 986 | } 987 | 988 | .SelectedCardContainer { 989 | height: 280px; 990 | width: 100%; 991 | display: flex; 992 | flex-direction: column; 993 | justify-content: center; 994 | } 995 | 996 | .SelectedCardGraphic { 997 | height: 220px; 998 | width: 380px; 999 | border-radius: 7px; 1000 | justify-self: center; 1001 | align-self: center; 1002 | position: relative; 1003 | top: 40px; 1004 | } 1005 | 1006 | .CardOptions { 1007 | width: 100%; 1008 | display: flex; 1009 | flex-direction: row; 1010 | justify-content: space-evenly; 1011 | } 1012 | 1013 | .CardName { 1014 | position: relative; 1015 | top: -4px; 1016 | left: -186px; 1017 | /* width: 100%!important; */ 1018 | /* height: 20px!important; */ 1019 | text-align:end; 1020 | padding: none!important; 1021 | margin: none!important; 1022 | } 1023 | 1024 | /* HEADER */ 1025 | .airwolf-header { 1026 | margin: 0 auto; 1027 | background-color: rgb(0, 0, 0); 1028 | position: fixed; 1029 | width: 100%; 1030 | height: 60px; 1031 | overflow: hidden; 1032 | top: 0; 1033 | } 1034 | .airwolf-header2 { 1035 | margin: 0 auto; 1036 | background-color: rgb(255, 255, 255); 1037 | position: fixed; 1038 | width: 100%; 1039 | height: 60px; 1040 | overflow: hidden; 1041 | /* top: 60px; */ 1042 | display: flex; 1043 | flex-direction: row; 1044 | justify-content: space-between!important; 1045 | border-bottom: 1px solid rgba(0, 0, 0, 0.202); 1046 | z-index: 15; 1047 | cursor: pointer; 1048 | } 1049 | 1050 | 1051 | .airwolf-header2 img { 1052 | height: 40px; 1053 | top:5px; 1054 | position: relative; 1055 | padding-left: 30px; 1056 | } 1057 | 1058 | .airwolf-header2 button { 1059 | position: absolute; 1060 | right: 0px; 1061 | height: 60px; 1062 | } 1063 | 1064 | .BedrockImage { 1065 | height: 70px!important; 1066 | position: relative; 1067 | margin-top: -15px; 1068 | margin-left: 30px; 1069 | } 1070 | 1071 | .OtherModels { 1072 | display: flex; 1073 | flex-direction: row; 1074 | width: 80vw; 1075 | /* justify-content: space-evenly; */ 1076 | padding-top: 0px!important; 1077 | position: relative; 1078 | /* left: 20px; */ 1079 | } 1080 | 1081 | .model-option { 1082 | font-size: 22px!important; 1083 | padding: 10px 10px!important; 1084 | margin: 0px 30px 0px 0px; 1085 | } 1086 | 1087 | .bedrockDemo { 1088 | height: 60px!important; 1089 | padding-top: 0px!important; 1090 | } 1091 | 1092 | .model-option:hover { 1093 | cursor: pointer; 1094 | border-bottom: 6px solid #36dca4!important; 1095 | } 1096 | 1097 | .model-option.active { 1098 | border-bottom: 6px solid #1c7c5c!important; 1099 | } 1100 | 1101 | .airwolf-header2 * { 1102 | padding-left:60px; 1103 | font-size: 26px; 1104 | height: 60px; 1105 | vertical-align:middle; 1106 | padding-top:10px; 1107 | 1108 | } 1109 | .airwolf-header::after { 1110 | z-index: 1; 1111 | /* box-shadow: 0 0 240px 240px rgba(0,0,0,.5); */ 1112 | content: ''; 1113 | background: red; 1114 | width: 340px; 1115 | height: 340px; 1116 | position: absolute; 1117 | top: 100%; 1118 | left: 50%; 1119 | transform: translateX(-50%); 1120 | border-radius: 50%; 1121 | } 1122 | /* .airwolf-header-title { 1123 | z-index: 2; 1124 | font-family: "Amazon Ember", Helvetica, Arial, sans-serif; 1125 | letter-spacing: 2px; 1126 | position: absolute; 1127 | color: rgb(250,250,250); 1128 | top: 40px; 1129 | left: 50%; 1130 | transform: translate(-50%, -50%); 1131 | font-size: 40px; 1132 | line-height: 1.4; 1133 | font-weight: 700; 1134 | } 1135 | .airwolf-header-title::after { 1136 | z-index: 2; 1137 | display: block; 1138 | content: ''; 1139 | width: 100%; 1140 | height: 1px; 1141 | background: rgb(255,153,0); 1142 | } */ 1143 | .airwolf-header-subtitle { 1144 | z-index: 2; 1145 | font-family: "Amazon Ember", Helvetica, Arial, sans-serif; 1146 | letter-spacing: 2px; 1147 | position: absolute; 1148 | color: rgb(250,250,250); 1149 | top: 99px; 1150 | left: 50%; 1151 | transform: translate(-50%, -50%); 1152 | font-size: 20px; 1153 | line-height: 1.4; 1154 | font-weight: 700; 1155 | } 1156 | 1157 | .airwolf-header-stripe-shine { 1158 | position: absolute; 1159 | content: ''; 1160 | background: white; 1161 | width: 100%; 1162 | height: 20px; 1163 | transform: rotate(-70deg); 1164 | 1165 | animation-duration: 6s; 1166 | animation-name: shine; 1167 | animation-iteration-count: infinite; 1168 | } 1169 | .airwolf-header-stripe-shine.two { 1170 | height: 10px; 1171 | animation-duration: 3s; 1172 | } 1173 | 1174 | .airwolf-header-stripe-fade { 1175 | display: block; 1176 | width: 1000px; 1177 | height: 20px; 1178 | 1179 | background: rgb(236,114,17); 1180 | 1181 | margin-left: -20px; 1182 | margin-top: -20px; 1183 | 1184 | transform: rotate(-20deg); 1185 | animation-duration: 6s; 1186 | animation-name: fadeInOut; 1187 | animation-iteration-count: infinite; 1188 | 1189 | 1190 | } 1191 | /* .airwolf-header-stripe-fade.one { 1192 | background: rgb(255,153,0); 1193 | margin-bottom: 10px; 1194 | animation-duration: 6s; 1195 | } 1196 | .airwolf-header-stripe-fade.two { 1197 | background: rgb(236,114,17); 1198 | margin-bottom: 75px; 1199 | animation-duration: 17s; 1200 | } 1201 | .airwolf-header-stripe-fade.three { 1202 | background: rgb(236,114,17); 1203 | margin-bottom: 10px; 1204 | animation-duration: 7s; 1205 | } 1206 | .airwolf-header-stripe-fade.four { 1207 | animation-name: fadeInOutDark; 1208 | background: rgb(35,47,62); 1209 | margin-bottom: 30px; 1210 | animation-duration: 9s; 1211 | } 1212 | .airwolf-header-stripe-fade.five { 1213 | background: rgb(255,153,0); 1214 | margin-bottom: 150px; 1215 | animation-duration: 13s; 1216 | } */ 1217 | 1218 | @keyframes shine { 1219 | 0% { 1220 | transform: rotate(-90deg); 1221 | left: -100%; 1222 | background: rgba(255,255,255,0); 1223 | } 1224 | 1225 | 15% { 1226 | transform: rotate(-70deg); 1227 | } 1228 | 1229 | 25% { 1230 | left: 50%; 1231 | background: rgba(255,255,255,0.05); 1232 | } 1233 | 1234 | 100% { 1235 | transform: rotate(-90deg); 1236 | left: 150%; 1237 | background: rgba(255,255,255,0); 1238 | } 1239 | } 1240 | 1241 | #menuview .navbar-brand { 1242 | width: 100px; 1243 | height: 25px; 1244 | margin-left: 5px; 1245 | display: block; 1246 | background-size: 100%; 1247 | background-image: url('data:image/png;base64,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'); 1248 | } 1249 | 1250 | #menuview .navbar-brand img { 1251 | display: none; 1252 | } 1253 | 1254 | .navbar li.navbar-help-menu > a.dropdown-toggle { 1255 | color: var(--awsui-grey-900); 1256 | } 1257 | 1258 | @keyframes fadeInOut { 1259 | 0% { 1260 | opacity: 0; 1261 | } 1262 | 1263 | 50% { 1264 | opacity: 0.9; 1265 | } 1266 | 1267 | 100% { 1268 | opacity: 0; 1269 | } 1270 | } 1271 | 1272 | @keyframes fadeInOutDark { 1273 | 0% { 1274 | opacity: 0; 1275 | } 1276 | 1277 | 50% { 1278 | opacity: .5; 1279 | } 1280 | 1281 | 100% { 1282 | opacity: 0; 1283 | } 1284 | } 1285 | 1286 | #JobModalContainer { 1287 | z-index: 99; 1288 | position: fixed; 1289 | top: 0px; 1290 | left: 0; 1291 | height: 100vh; 1292 | width: 100vw; 1293 | flex-direction: column; 1294 | justify-content: center; 1295 | align-items: center; 1296 | align-content: center; 1297 | overflow: hidden; 1298 | background: rgba(114, 114, 113, 0.5); 1299 | display:flex; 1300 | flex-direction: column; 1301 | box-sizing: border-box; 1302 | } 1303 | 1304 | #JobModal { 1305 | z-index: 100; 1306 | width: 500px; 1307 | position: relative; 1308 | top: -14vh; 1309 | overflow: hidden; 1310 | background: white; 1311 | -webkit-box-shadow: 0px 10px 38px -8px rgba(0,0,0,0.75); 1312 | -moz-box-shadow: 0px 10px 38px -8px rgba(0,0,0,0.75); 1313 | box-shadow: 0px 10px 38px -8px rgba(0,0,0,0.75); 1314 | display:flex; 1315 | flex-direction: column; 1316 | border: 1px solid black; 1317 | transition: opacity 0.3s linear, max-height 0.2s linear; 1318 | box-sizing: border-box; 1319 | } 1320 | 1321 | .JobModalContent button { 1322 | margin-bottom: 40px; 1323 | } 1324 | 1325 | .JobModalNav { 1326 | justify-content: flex-end!important; 1327 | } 1328 | 1329 | #ModalUpperNav * { 1330 | align-items: center; 1331 | vertical-align: middle; 1332 | display: flex; 1333 | border: none; 1334 | } 1335 | 1336 | #ModalUpperNav span { 1337 | padding-left: 20px; 1338 | font-size: 20px!important; 1339 | font-weight: 600; 1340 | letter-spacing: 1px; 1341 | } 1342 | 1343 | #ModalUpperNav button { 1344 | color: white; 1345 | border-radius: none!important; 1346 | } 1347 | 1348 | #ModalUpperNav button:hover { 1349 | color: red; 1350 | border-radius: none!important; 1351 | } 1352 | 1353 | .lds-spinner { 1354 | color: official; 1355 | display: inline-block; 1356 | position: relative; 1357 | width: 80px; 1358 | height: 80px; 1359 | margin-bottom: 40px!important; 1360 | } 1361 | .lds-spinner div { 1362 | transform-origin: 40px 40px; 1363 | animation: lds-spinner 1.2s linear infinite; 1364 | } 1365 | .lds-spinner div:after { 1366 | content: " "; 1367 | display: block; 1368 | position: absolute; 1369 | top: 3px; 1370 | left: 37px; 1371 | width: 6px; 1372 | height: 18px; 1373 | border-radius: 20%; 1374 | background: rgba(35, 47, 62, 1); 1375 | } 1376 | .lds-spinner div:nth-child(1) { 1377 | transform: rotate(0deg); 1378 | animation-delay: -1.1s; 1379 | } 1380 | .lds-spinner div:nth-child(2) { 1381 | transform: rotate(30deg); 1382 | animation-delay: -1s; 1383 | } 1384 | .lds-spinner div:nth-child(3) { 1385 | transform: rotate(60deg); 1386 | animation-delay: -0.9s; 1387 | } 1388 | .lds-spinner div:nth-child(4) { 1389 | transform: rotate(90deg); 1390 | animation-delay: -0.8s; 1391 | } 1392 | .lds-spinner div:nth-child(5) { 1393 | transform: rotate(120deg); 1394 | animation-delay: -0.7s; 1395 | } 1396 | .lds-spinner div:nth-child(6) { 1397 | transform: rotate(150deg); 1398 | animation-delay: -0.6s; 1399 | } 1400 | .lds-spinner div:nth-child(7) { 1401 | transform: rotate(180deg); 1402 | animation-delay: -0.5s; 1403 | } 1404 | .lds-spinner div:nth-child(8) { 1405 | transform: rotate(210deg); 1406 | animation-delay: -0.4s; 1407 | } 1408 | .lds-spinner div:nth-child(9) { 1409 | transform: rotate(240deg); 1410 | animation-delay: -0.3s; 1411 | } 1412 | .lds-spinner div:nth-child(10) { 1413 | transform: rotate(270deg); 1414 | animation-delay: -0.2s; 1415 | } 1416 | .lds-spinner div:nth-child(11) { 1417 | transform: rotate(300deg); 1418 | animation-delay: -0.1s; 1419 | } 1420 | .lds-spinner div:nth-child(12) { 1421 | transform: rotate(330deg); 1422 | animation-delay: 0s; 1423 | } 1424 | @keyframes lds-spinner { 1425 | 0% { 1426 | opacity: 1; 1427 | } 1428 | 100% { 1429 | opacity: 0; 1430 | } 1431 | } 1432 | 1433 | .dots { 1434 | width: 50px; 1435 | height: 20px; 1436 | border-radius: 10px; 1437 | /* background-color: #CCC; */ 1438 | margin: 10px; 1439 | transform: scale(0); 1440 | animation: overall-scale 8s infinite; 1441 | margin-left: 45%; 1442 | } 1443 | 1444 | .dot { 1445 | animation-timing-function: ease-in; 1446 | animation-iteration-count: infinite; 1447 | animation-name: dot-scale; 1448 | animation-duration: 1s; 1449 | 1450 | display: inline-block; 1451 | width: 10px; 1452 | height: 10px; 1453 | margin: 5px 0px; 1454 | border-radius: 10px; 1455 | background-color: #444; 1456 | transform: scale(0.65); 1457 | 1458 | &:first-of-type { 1459 | margin-left: 6px 1460 | } 1461 | &:nth-of-type(2) { 1462 | animation-delay: 0.15s; 1463 | } 1464 | &:nth-of-type(3) { 1465 | animation-delay: 0.3s; 1466 | } 1467 | } 1468 | 1469 | @keyframes dot-scale { 1470 | 0%, 70% { transform: scale(0.65); } 1471 | 35% { transform: scale(1); } 1472 | } 1473 | 1474 | @keyframes overall-scale { 1475 | 0%, 95% { transform: scale(0); } 1476 | 5%, 90% { transform: scale(1); } 1477 | } 1478 | 1479 | -------------------------------------------------------------------------------- /source/frontend/src/App.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT. 3 | 4 | import './App.css'; 5 | import '@aws-amplify/ui-react/styles.css'; 6 | import React, { useState, useEffect } from 'react'; 7 | import { Button, withAuthenticator } from '@aws-amplify/ui-react'; 8 | import { Amplify, Auth } from 'aws-amplify'; 9 | 10 | import { API_GATEWAY_URL } from './config'; 11 | 12 | const fileToB64 = file => new Promise((resolve, reject) => { 13 | const reader = new FileReader(); 14 | reader.readAsBinaryString(file); 15 | reader.onload = () => resolve(btoa(reader.result)); 16 | reader.onerror = reject; 17 | }); 18 | 19 | 20 | function App({ signOut, user }) { 21 | const [modelSelected, setModelSelected] = useState('Anthropic: Claude'); 22 | const [inputText, setInputText] = useState(""); 23 | const [responseText, setResponseText] = useState(""); 24 | const [imageResponse, setImageResponse] = useState(null); 25 | const [imageResponseImg, setImageResponseImg] = useState(""); 26 | const [isBuffering, setIsBuffering] = useState(false) 27 | const [isBufferingLanguage, setIsBufferingLanguage] = useState(false) 28 | const [languageResponse, setLanguageResponse] = useState(""); 29 | const [translationQueue, setTranslationQueue] = useState([]); 30 | const [languageSelected, setLanguageSelected] = useState("English"); 31 | const [demoSelected, setDemoSelected] = useState("Image"); 32 | const [userInputPrompt, setUserInputPrompt] = useState(''); 33 | const [imageBlob, setImageBlob] = useState(''); 34 | const [userInputEnhance, setUserInputEnhance] = useState(''); 35 | const [enhanceResponse, setEnhanceResponse] = useState(''); 36 | const [authData, setAuthData] = useState({}) 37 | 38 | async function getSession() { 39 | try { 40 | const getAuth = await Auth.currentSession(); 41 | console.log(getAuth); 42 | console.log(getAuth.idToken.jwtToken) 43 | setAuthData(getAuth.idToken.jwtToken) 44 | } catch (error) { 45 | console.log(error); 46 | } 47 | } 48 | 49 | 50 | 51 | useEffect(() => { 52 | getSession(); 53 | 54 | }, []) 55 | 56 | const handleImageFormSubmit = (event) => { 57 | event.preventDefault(); 58 | setIsBuffering(true) 59 | fileToB64(event.target.files[0]) 60 | .then((b64) => fetch(API_GATEWAY_URL +"/bedrock", { 61 | // /call-rekognition-api 62 | method: "POST", 63 | headers: { 64 | "Content-Type": "application/json", 65 | "Authorization": `Bearer ${authData}` 66 | }, 67 | body: JSON.stringify({'image': b64, 'endpoint': '/api/call-rekognition-api'}), 68 | }) 69 | ) 70 | .then((response) => response.json()) 71 | .then((data) => { 72 | console.log(JSON.parse(data.body).labels) 73 | // console.log(data.labels) 74 | setImageResponse(URL.createObjectURL(event.target.files[0])); 75 | const output = JSON.parse(data.body).labels.join(" "); 76 | setInputText(`Build me a product Description for ${output}`); 77 | let payload = { 78 | modelId: 'anthropic.claude-instant-v1', 79 | contentType: 'application/json', 80 | accept: '*/*', 81 | endpoint: '/api/conversation/predict-claude', 82 | body: JSON.stringify({ 83 | prompt: `Build me a product Description using 120 words or less. Here is additional information about the product photograph ${output}`, 84 | max_tokens_to_sample: 300, 85 | temperature: 0.5, 86 | top_k: 250, 87 | top_p: 1, 88 | stop_sequences: ['\n\nHuman:'] 89 | }), 90 | }; 91 | fetch(API_GATEWAY_URL + '/bedrock', { 92 | method: 'POST', 93 | headers: { 94 | 'Content-Type': 'application/json', 95 | "Authorization": `Bearer ${authData}` 96 | }, 97 | body: JSON.stringify(payload) 98 | }) 99 | .then((response) => response.json()) 100 | .then((data) => { 101 | 102 | setResponseText(JSON.parse(data.body)) 103 | handleTranslations(JSON.parse(data.body)) 104 | } 105 | ) 106 | .catch((error) => console.error("Error:", error)) 107 | .finally( 108 | setTimeout(function () { 109 | setIsBuffering(false) 110 | }, 2000) 111 | ) 112 | }) 113 | .catch((error) => console.error("Error:", error)) 114 | 115 | }; 116 | 117 | const base64ToBlob = (base64Data, contentType = "image/jpeg") => { 118 | const byteCharacters = atob(base64Data); 119 | const byteArrays = []; 120 | 121 | for (let offset = 0; offset < byteCharacters.length; offset += 512) { 122 | const slice = byteCharacters.slice(offset, offset + 512); 123 | 124 | const byteNumbers = new Array(slice.length); 125 | for (let i = 0; i < slice.length; i++) { 126 | byteNumbers[i] = slice.charCodeAt(i); 127 | } 128 | 129 | const byteArray = new Uint8Array(byteNumbers); 130 | byteArrays.push(byteArray); 131 | } 132 | 133 | return new Blob(byteArrays, { type: contentType }); 134 | }; 135 | 136 | const handleImageFormSubmitPrompt = (blob) => { 137 | setIsBuffering(true) 138 | setLanguageResponse("") 139 | fetch(API_GATEWAY_URL + '/bedrock', { 140 | method: "POST", 141 | headers: { 142 | "Content-Type": "application/json", 143 | "Authorization": `Bearer ${authData}` 144 | }, 145 | body: JSON.stringify({'image': blob, 'endpoint': "/api/call-rekognition-api"}), 146 | }) 147 | .then((response) => response.json()) 148 | .then((data) => { 149 | const output = JSON.parse(data.body).labels.join(" "); 150 | setInputText(`Build me a product Description for ${output}`); 151 | let payload = { 152 | modelId: 'anthropic.claude-instant-v1', 153 | contentType: 'application/json', 154 | endpoint: '/api/conversation/predict-claude', 155 | accept: '*/*', 156 | body: JSON.stringify({ 157 | prompt: `Build me a product Description using 120 words or less. Here is additional information about the product photograph ${output}`, 158 | max_tokens_to_sample: 300, 159 | temperature: 0.5, 160 | top_k: 250, 161 | top_p: 1, 162 | stop_sequences: ['\n\nHuman:'] 163 | }), 164 | }; 165 | fetch(API_GATEWAY_URL + '/bedrock', { 166 | method: "POST", 167 | headers: { 168 | "Content-Type": "application/json", 169 | "Authorization": `Bearer ${authData}` 170 | }, 171 | body: JSON.stringify(payload), 172 | }) 173 | .then((response) => response.json()) 174 | .then((data) => { 175 | setResponseText(JSON.parse(data.body)) 176 | handleTranslations(JSON.parse(data.body)) 177 | }) 178 | .catch((error) => console.error("Error:", error)) 179 | .finally( 180 | setIsBuffering(false) 181 | ) 182 | }) 183 | .catch((error) => console.error("Error:", error)) 184 | 185 | }; 186 | 187 | const handleImageChange = (event) => { 188 | setLanguageResponse("") 189 | setLanguageSelected("English") 190 | const imageFile = event.target.files[0]; 191 | if (imageFile) { 192 | handleImageFormSubmit(event); 193 | } 194 | }; 195 | 196 | const handleSubmit = (event) => { 197 | event.preventDefault(); 198 | setIsBuffering(true); 199 | setLanguageResponse("") 200 | setImageResponseImg("") 201 | const payload = { 202 | "modelId": "stability.stable-diffusion-xl", 203 | "contentType": "application/json", 204 | "accept": "application/json", 205 | 'endpoint': "/api/call-stablediffusion", 206 | "body": JSON.stringify({ 207 | "text_prompts": [ 208 | { "text": userInputPrompt } 209 | ], 210 | "cfg_scale": 10, 211 | "seed": 0, 212 | "steps": 50 213 | }) 214 | }; 215 | 216 | fetch(API_GATEWAY_URL+'/bedrock', { 217 | method: "POST", 218 | headers: { 219 | "Content-Type": "application/json", 220 | "Authorization": `Bearer ${authData}` 221 | }, 222 | body: JSON.stringify(payload) 223 | }) 224 | .then(response => response.json()) 225 | .then(data => { 226 | if (JSON.parse(data.body).result === "success" && JSON.parse(data.body).artifacts.length > 0) { 227 | const base64Data = JSON.parse(data.body).artifacts[0].base64; 228 | const imageUrl = "data:image/jpeg;base64," + base64Data; 229 | setImageResponseImg(Generated Image); 230 | 231 | setImageBlob(base64Data); 232 | handleImageFormSubmitPrompt(base64Data); 233 | } else { 234 | setImageResponseImg("Image API failed to generate the image."); 235 | } 236 | }) 237 | // .catch(() => { 238 | // setImageResponseImg("Image API request failed."); 239 | // }) 240 | .finally(() => { 241 | setIsBuffering(false) 242 | }); 243 | }; 244 | 245 | const sendMessageEnhance = () => { 246 | setIsBuffering(true); 247 | setEnhanceResponse('') 248 | 249 | let payload = { 250 | modelId: 'anthropic.claude-instant-v1', 251 | contentType: 'application/json', 252 | endpoint: '/api/conversation/predict-claude', 253 | accept: '*/*', 254 | body: JSON.stringify({ 255 | prompt: `I will be listing a product on my eCommerce Marketplace. Make the following product description better: ${userInputEnhance}`, 256 | max_tokens_to_sample: 300, 257 | temperature: 0.5, 258 | top_k: 250, 259 | top_p: 1, 260 | stop_sequences: ['\n\nHuman:'] 261 | }), 262 | }; 263 | 264 | // setUserInput(''); 265 | fetch(API_GATEWAY_URL +'/bedrock', { 266 | method: 'POST', 267 | headers: { 268 | 'Content-Type': 'application/json', 269 | "Authorization": `Bearer ${authData}` 270 | }, 271 | body: JSON.stringify(payload) 272 | }) 273 | .then(response => response.json()) 274 | .then(response => { 275 | setEnhanceResponse(JSON.parse(response.body)) 276 | }) 277 | .catch(error => { 278 | console.error('Error:', error); 279 | }).finally(() => { 280 | setIsBuffering(false) 281 | }); 282 | }; 283 | 284 | const handleTranslations = (description) => { 285 | setIsBufferingLanguage(true); 286 | 287 | let languages = ['Spanish', 'French', 'German']; 288 | 289 | let payload = { 290 | prompt: '', 291 | maxTokens: 200, 292 | temperature: 0.5, 293 | topP: 0.5, 294 | stopSequences: [], 295 | countPenalty: { scale: 0 }, 296 | presencePenalty: { scale: 0 }, 297 | frequencyPenalty: { scale: 0 } 298 | }; 299 | 300 | let response_container = [ 301 | { 302 | 'Language': 'English', 303 | 'Description': description 304 | } 305 | ]; 306 | setLanguageResponse(response_container) 307 | let newQueue = []; 308 | 309 | for (let i = 0; i < languages.length; i++) { 310 | payload.prompt = `Translate this to ${languages[i]}: ${description}`; 311 | 312 | callai21(languages[i], payload) 313 | } 314 | 315 | // setTranslationQueue((prevQueue) => [...prevQueue, ...newQueue]); 316 | processTranslationQueue(); 317 | }; 318 | 319 | const processTranslationQueue = () => { 320 | if (translationQueue.length > 0) { 321 | const nextRequest = translationQueue[0]; 322 | 323 | setTranslationQueue((prevQueue) => prevQueue.slice(1)); 324 | 325 | nextRequest 326 | .then((response) => { 327 | console.log(response); 328 | }) 329 | .catch((error) => { 330 | console.error('Error:', error); 331 | }) 332 | .finally(() => { 333 | processTranslationQueue(); 334 | }); 335 | } else { 336 | setIsBufferingLanguage(false); 337 | } 338 | }; 339 | 340 | const callai21 = async (language, payload) => { 341 | // setIsBufferingLanguage(true); 342 | console.log('payload in callai21 start:'); 343 | console.log(payload); 344 | payload['endpoint'] = '/api/conversation/predict-ai21' 345 | console.log('payload in callai21 end'); 346 | console.log(''); 347 | 348 | try { 349 | const response = await fetch(API_GATEWAY_URL+'/bedrock', { 350 | method: 'POST', 351 | headers: { 352 | 'Content-Type': 'application/json', 353 | "Authorization": `Bearer ${authData}` 354 | }, 355 | body: JSON.stringify(payload) 356 | }); 357 | 358 | if (!response.ok) { 359 | throw new Error(`HTTP error! Status: ${response.status}`); 360 | } 361 | 362 | const responseData = await response.json(); 363 | const botResponse = JSON.parse(responseData.body).output_text; 364 | console.log(language); 365 | console.log(botResponse); 366 | 367 | setLanguageResponse(prevLanguageResponse => [ 368 | ...prevLanguageResponse, 369 | { 370 | 'Language': language, 371 | 'Description': botResponse 372 | } 373 | ]); 374 | } catch (error) { 375 | console.error('Error:', error); 376 | } finally { 377 | setIsBufferingLanguage(false); 378 | setIsBuffering(false) 379 | } 380 | }; 381 | 382 | 383 | const getSelectedDescription = () => { 384 | for (let i = 0; i < languageResponse.length; i++) { 385 | if (languageResponse[i].Language == languageSelected) { 386 | return languageResponse[i].Description 387 | } 388 | } 389 | } 390 | 391 | const handleDemoSelection = (demo) => { 392 | setResponseText('') 393 | setDemoSelected(demo) 394 | setLanguageResponse('') 395 | setImageResponseImg('') 396 | setLanguageSelected('English') 397 | setEnhanceResponse('') 398 | setUserInputPrompt('') 399 | setUserInputEnhance('') 400 | } 401 | 402 | return ( 403 | 404 |
405 |
406 |

Building Product Descriptions with Amazon Bedrock

407 |
408 |
409 | handleDemoSelection('Image')}> 411 | Create Product Description from Image 412 | 413 | handleDemoSelection('Prompt')}> 415 | Create Image and Description from Text 416 | 417 | handleDemoSelection('Enhance')}> 419 | Enhance a Product Description 420 | 421 |
422 |
423 | {demoSelected === 'Image' && 424 |
425 | 435 |
436 | } 437 | {demoSelected === 'Prompt' && 438 |
439 |