[
  {
    "path": ".gitignore",
    "content": "\n# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode\n\n### SAM ###\n.aws-sam/\nsamconfig.toml\n\n### Linux ###\n*~\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n### OSX ###\n*.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### PyCharm ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff:\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/dictionaries\n\n# Sensitive or high-churn files:\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.xml\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n\n# Gradle:\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# CMake\ncmake-build-debug/\n\n# Mongo Explorer plugin:\n.idea/**/mongoSettings.xml\n\n## File-based project format:\n*.iws\n\n## Plugin-specific files:\n\n# IntelliJ\n/out/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Ruby plugin and RubyMine\n/.rakeTasks\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n### PyCharm Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n.idea/sonarlint\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\n.pytest_cache/\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule.*\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n### VisualStudioCode ###\n.vscode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n.history\n\n### Windows ###\n# Windows thumbnail cache files\nThumbs.db\nehthumbs.db\nehthumbs_vista.db\n\n# Folder config file\nDesktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# Build folder\n\n*/build/*\n\n# End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode\n\nevents/*.json\ndependencies"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "## Code of Conduct\nThis project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).\nFor more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact\nopensource-codeofconduct@amazon.com with any additional questions or comments.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing Guidelines\n\nThank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional\ndocumentation, we greatly value feedback and contributions from our community.\n\nPlease read through this document before submitting any issues or pull requests to ensure we have all the necessary\ninformation to effectively respond to your bug report or contribution.\n\n\n## Reporting Bugs/Feature Requests\n\nWe welcome you to use the GitHub issue tracker to report bugs or suggest features.\n\nWhen filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already\nreported the issue. Please try to include as much information as you can. Details like these are incredibly useful:\n\n* A reproducible test case or series of steps\n* The version of our code being used\n* Any modifications you've made relevant to the bug\n* Anything unusual about your environment or deployment\n\n\n## Contributing via Pull Requests\nContributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:\n\n1. You are working against the latest source on the *main* branch.\n2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.\n3. You open an issue to discuss any significant work - we would hate for your time to be wasted.\n\nTo send us a pull request, please:\n\n1. Fork the repository.\n2. 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.\n3. Ensure local tests pass.\n4. Commit to your fork using clear commit messages.\n5. Send us a pull request, answering any default questions in the pull request interface.\n6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.\n\nGitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and\n[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).\n\n\n## Finding contributions to work on\nLooking 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.\n\n\n## Code of Conduct\nThis project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).\nFor more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact\nopensource-codeofconduct@amazon.com with any additional questions or comments.\n\n\n## Security issue notifications\nIf 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.\n\n\n## Licensing\n\nSee the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT No Attribution\n\nCopyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# Serverless document chat application\n\nThis sample application allows you to ask natural language questions of any PDF document you upload. It combines the text generation and analysis capabilities of an LLM with a vector search of the document content. The solution uses serverless services such as [Amazon Bedrock](https://aws.amazon.com/bedrock/) to access foundational models, [AWS Lambda](https://aws.amazon.com/lambda/) to run [LangChain](https://github.com/langchain-ai/langchain), and [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) for conversational memory.\n\nSee the [accompanying blog post on the AWS Serverless Blog](https://aws.amazon.com/blogs/compute/building-a-serverless-document-chat-with-aws-lambda-and-amazon-bedrock/) for a detailed description and follow the deployment instructions below to get started.\n\n<p float=\"left\">\n  <img src=\"preview-1.png\" width=\"49%\" />\n  <img src=\"preview-2.png\" width=\"49%\" />\n</p>\n\n> **Warning**\n> This application is not ready for production use. It was written for demonstration and educational purposes. Review the [Security](#security) section of this README and consult with your security team before deploying this stack. No warranty is implied in this example.\n\n> **Note**\n> This architecture creates resources that have costs associated with them. Please see the [AWS Pricing](https://aws.amazon.com/pricing/) page for details and make sure to understand the costs before deploying this stack.\n\n## Key features\n\n- [Amazon Bedrock](https://aws.amazon.com/de/bedrock/) for serverless embedding and inference\n- [LangChain](https://github.com/hwchase17/langchain) to orchestrate a Q&A LLM chain\n- [FAISS](https://github.com/facebookresearch/faiss) vector store\n- [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) for serverless conversational memory\n- [AWS Lambda](https://aws.amazon.com/lambda/) for serverless compute\n- Frontend built in [React](https://react.dev/), [TypeScript](https://www.typescriptlang.org/), [TailwindCSS](https://tailwindcss.com/), and [Vite](https://vitejs.dev/).\n- Run locally or deploy to [AWS Amplify Hosting](https://aws.amazon.com/amplify/hosting/)\n- [Amazon Cognito](https://aws.amazon.com/cognito/) for authentication\n\n## How the application works\n\n![Serverless PDF Chat architecture](architecture.png \"Serverless PDF Chat architecture\")\n\n1. A user uploads a PDF document into an [Amazon Simple Storage Service](https://aws.amazon.com/s3/) (S3) bucket through a static web application frontend.\n1. This upload triggers a metadata extraction and document embedding process. The process converts the text in the document into vectors. The vectors are loaded into a vector index and stored in S3 for later use.\n1. When a user chats with a PDF document and sends a prompt to the backend, a Lambda function retrieves the index from S3 and searches for information related to the prompt.\n1. A LLM then uses the results of this vector search, previous messages in the conversation, and its general-purpose capabilities to formulate a response to the user.\n\n## Deployment instructions\n\n### Prerequisites\n\n- [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html)\n- [Python](https://www.python.org/) 3.11 or greater\n\n### Cloning the repository\n\nClone this repository:\n\n```bash\ngit clone https://github.com/aws-samples/serverless-pdf-chat.git\n```\n\n### Amazon Bedrock setup\n\nThis application can be used with a variety of Amazon Bedrock models. See [Supported models in Amazon Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-service.html#models-supported) for a complete list.\n\nBy default, this application uses **Titan Embeddings G1 - Text** to generate embeddings and **Anthropic Claude v3 Sonnet** for responses.\n\n> **Important -**\n> Before you can use these models with this application, **you must request access in the Amazon Bedrock console**. See the [Model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) section of the Bedrock User Guide for detailed instructions.\n> By default, this application is configured to use Amazon Bedrock in the `us-east-1` Region, make sure you request model access in that Region (this does not have to be the same Region that you deploy this stack to).\n\nTo select your Bedrock model, specify the `ModelId` parameter during the AWS SAM deployment, such as `anthropic.claude-3-sonnet-20240229-v1:0`. See [Amazon Bedrock model IDs](https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html) for a complete list.\n\nThe `ModelId` parameter is used in the GenerateResponseFunction Lambda function of your AWS SAM template to instantiate [LangChain BedrockChat](https://js.langchain.com/v0.1/docs/integrations/chat/bedrock/) and [ConversationalRetrievalChain](https://api.python.langchain.com/en/latest/chains/langchain.chains.conversational_retrieval.base.ConversationalRetrievalChain.html) objects, providing efficient retrieval of relevant context from large PDF datasets to enable the Bedrock model-generated response.\n\n```python\ndef bedrock_chain(faiss_index, memory, human_input, bedrock_runtime):\n\n    chat = BedrockChat(\n        model_id=MODEL_ID,\n        model_kwargs={'temperature': 0.0}\n    )\n\n    chain = ConversationalRetrievalChain.from_llm(\n        llm=chat,\n        chain_type=\"stuff\",\n        retriever=faiss_index.as_retriever(),\n        memory=memory,\n        return_source_documents=True,\n    )\n\n    response = chain.invoke({\"question\": human_input})\n\n    return response\n```\n\n### Deploy the frontend with AWS Amplify Hosting\n\n[AWS Amplify Hosting](https://aws.amazon.com/amplify/hosting/) enables a fully-managed deployment of the application's React frontend in an AWS-managed account using Amazon S3 and [Amazon CloudFront](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html). You can optionally run the React frontend locally by skipping to [Deploy the application with AWS SAM](#Deploy-the-application-with-AWS-SAM).\n\nTo set up Amplify Hosting:\n\n1. Fork this GitHub repository and take note of your repository URL, for example `https://github.com/user/serverless-pdf-chat/`.\n1. Create a GitHub fine-grained access token for the new repository by following [this guide](https://docs.aws.amazon.com/amplify/latest/userguide/setting-up-GitHub-access.html). For the **Repository permissions**, select **Read and write** for **Content** and **Webhooks**.\n1. Create a new secret called `serverless-pdf-chat-github-token` in AWS Secrets Manager and input your fine-grained access token as plaintext. Select the **Plaintext** tab and confirm your secret looks like this:\n\n   ```json\n   github_pat_T2wyo------------------------------------------------------------------------rs0Pp\n   ```\n\n### Deploy the application with AWS SAM\n\n1. Change to the `backend` directory and [build](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-build.html) the application:\n\n   ```bash\n   cd backend\n   sam build\n   ```\n\n1. [Deploy](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-deploy.html) the application into your AWS account:\n\n   ```bash\n   sam deploy --guided\n   ```\n\n1. For **Stack Name**, choose `serverless-pdf-chat`.\n\n1. For **Frontend**, specify the environment (\"local\", \"amplify\") for the frontend of the application.\n\n1. If you selected \"amplify\", specify the URL of the forked Git repository containing the application code.\n\n1. Specify the Amazon Bedrock model ID. For example, `anthropic.claude-3-sonnet-20240229-v1:0`.\n\n1. For the remaining options, keep the defaults by pressing the enter key.\n\nAWS SAM will now provision the AWS resources defined in the `backend/template.yaml` template. Once the deployment is completed successfully, you will see a set of output values similar to the following:\n\n```bash\nCloudFormation outputs from deployed stack\n-------------------------------------------------------------------------------\nOutputs\n-------------------------------------------------------------------------------\nKey                 CognitoUserPool\nDescription         -\nValue               us-east-1_gxKtRocFs\n\nKey                 CognitoUserPoolClient\nDescription         -\nValue               874ghcej99f8iuo0lgdpbrmi76k\n\nKey                 ApiGatewayBaseUrl\nDescription         -\nValue               https://abcd1234.execute-api.us-east-1.amazonaws.com/dev/\n-------------------------------------------------------------------------------\n```\n\nIf you selected to deploy the React frontend using Amplify Hosting, navigate to the Amplify console to check the build status. If the build does not start automatically, trigger it through the Amplify console.\n\nIf you selected to run the React frontend locally and connect to the deployed resources in AWS, you will use the CloudFormation stack outputs in the following section.\n\n### Optional: Run the React frontend locally\n\nCreate a file named `.env.development` in the `frontend` directory. [Vite will use this file](https://vitejs.dev/guide/env-and-mode.html) to set up environment variables when we run the application locally.\n\nCopy the following file content and replace the values with the outputs provided by AWS SAM:\n\n```plaintext\nVITE_REGION=us-east-1\nVITE_API_ENDPOINT=https://abcd1234.execute-api.us-east-1.amazonaws.com/dev/\nVITE_USER_POOL_ID=us-east-1_gxKtRocFs\nVITE_USER_POOL_CLIENT_ID=874ghcej99f8iuo0lgdpbrmi76k\n```\n\nNext, install the frontend's dependencies by running the following command in the `frontend` directory:\n\n```bash\nnpm ci\n```\n\nFinally, to start the application locally, run the following command in the `frontend` directory:\n\n```bash\nnpm run dev\n```\n\nVite will now start the application under `http://localhost:5173`.\n\n### Create a user in the Amazon Cognito user pool\n\nThe application uses Amazon Cognito to authenticate users through a login screen. In this step, you will create a user to access the application.\n\nPerform the following steps to create a user in the Cognito user pool:\n\n1. Navigate to the **Amazon Cognito console**.\n1. Find the user pool with an ID matching the output provided by AWS SAM above.\n1. Under Users, choose **Create user**.\n1. Enter an email address and a password that adheres to the password requirements.\n1. Choose **Create user**.\n\nNavigate back to your Amplify website URL or local host address to log in with the new user's credentials.\n\n## Cleanup\n\n1. Delete any secrets in AWS Secrets Manager created as part of this walkthrough.\n1. [Empty the Amazon S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/empty-bucket.html) created as part of the AWS SAM template.\n1. Run the following command in the `backend` directory of the project to delete all associated resources resources:\n\n   ```bash\n   sam delete\n   ```\n## Troubleshooting\n\nIf you are experiencing issues when running the [`sam build`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-build.html) command, try setting the `--use-container` flag (requires Docker):\n\n```bash\nsam build --use-container\n```\n\nIf you are still experiencing issues despite using `--use-container`, try switching the AWS Lambda functions from `arm64` to `x86_64` in the `backend/template.yaml` (as well as switching to the `x_86_64` version of Powertools):\n\n```yaml\nGlobals:\n  Function:\n    Runtime: python3.11\n    Handler: main.lambda_handler\n    Architectures:\n      - x86_64\n    Tracing: Active\n    Environment:\n      Variables:\n        LOG_LEVEL: INFO\n    Layers:\n      - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:51\n```\n\n## Security\n\nThis application was written for demonstration and educational purposes and not for production use. The [Security Pillar of the AWS Well-Architected Framework](https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/welcome.html) can support you in further adopting the sample into a production deployment in addition to your own established processes. Take note of the following:\n\n- The application uses encryption in transit and at rest with AWS-managed keys where applicable. Optionally, use [AWS KMS](https://aws.amazon.com/kms/) with [DynamoDB](https://docs.aws.amazon.com/kms/latest/developerguide/services-dynamodb.html), [SQS](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-server-side-encryption.html), and [S3](https://docs.aws.amazon.com/kms/latest/developerguide/services-s3.html) for more control over encryption keys.\n\n- This application uses [Powertools for AWS Lambda (Python)](https://github.com/aws-powertools/powertools-lambda-python) to log to inputs and ouputs to CloudWatch Logs. Per default, this can include sensitive data contained in user input. Adjust the log level and remove log statements to fit your security requirements.\n\n- [API Gateway access logging](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html#set-up-access-logging-using-console) and [usage plans](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html) are not activiated in this code sample. Similarly, [S3 access logging](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-loggingconfig.html) is currently not enabled.\n\n- In order to simplify the setup of the demo, this solution uses AWS managed policies associated to IAM roles that contain wildcards on resources. Please consider to further scope down the policies as you see fit according to your needs. Please note that there is a resource wildcard on the AWS managed `AWSLambdaSQSQueueExecutionRole`. This is a known behaviour, see [this GitHub issue](https://github.com/aws/serverless-application-model/issues/2118) for details.\n\n- If your security controls require inspecting network traffic, consider [adjusting the AWS SAM template](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html) to attach the Lambda functions to a VPC via its [`VpcConfig`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-vpcconfig.html).\n\nSee [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.\n\n## License\n\nThis library is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file.\n"
  },
  {
    "path": "backend/.gitignore",
    "content": "\n# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode\n\n### SAM ###\n.aws-sam/\nsamconfig.toml\n\n### Linux ###\n*~\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n### OSX ###\n*.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### PyCharm ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff:\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/dictionaries\n\n# Sensitive or high-churn files:\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.xml\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n\n# Gradle:\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# CMake\ncmake-build-debug/\n\n# Mongo Explorer plugin:\n.idea/**/mongoSettings.xml\n\n## File-based project format:\n*.iws\n\n## Plugin-specific files:\n\n# IntelliJ\n/out/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Ruby plugin and RubyMine\n/.rakeTasks\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n### PyCharm Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n.idea/sonarlint\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\n.pytest_cache/\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule.*\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n### VisualStudioCode ###\n.vscode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n.history\n\n### Windows ###\n# Windows thumbnail cache files\nThumbs.db\nehthumbs.db\nehthumbs_vista.db\n\n# Folder config file\nDesktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# Build folder\n\n*/build/*\n\n# End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode"
  },
  {
    "path": "backend/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/add_conversation/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/add_conversation/main.py",
    "content": "import os, json\nfrom datetime import datetime\nimport boto3\nimport shortuuid\nfrom aws_lambda_powertools import Logger\n\nDOCUMENT_TABLE = os.environ[\"DOCUMENT_TABLE\"]\nMEMORY_TABLE = os.environ[\"MEMORY_TABLE\"]\n\n\nddb = boto3.resource(\"dynamodb\")\ndocument_table = ddb.Table(DOCUMENT_TABLE)\nmemory_table = ddb.Table(MEMORY_TABLE)\nlogger = Logger()\n\n\n@logger.inject_lambda_context(log_event=True)\ndef lambda_handler(event, context):\n    user_id = event[\"requestContext\"][\"authorizer\"][\"claims\"][\"sub\"]\n    document_id = event[\"pathParameters\"][\"documentid\"]\n\n    response = document_table.get_item(\n        Key={\"userid\": user_id, \"documentid\": document_id}\n    )\n    conversations = response[\"Item\"][\"conversations\"]\n    logger.info({\"conversations\": conversations})\n\n    conversation_id = shortuuid.uuid()\n    timestamp = datetime.utcnow()\n    timestamp_str = timestamp.strftime(\"%Y-%m-%dT%H:%M:%S.%fZ\")\n    conversation = {\n        \"conversationid\": conversation_id,\n        \"created\": timestamp_str,\n    }\n    conversations.append(conversation)\n    logger.info({\"conversation_new\": conversation})\n    document_table.update_item(\n        Key={\"userid\": user_id, \"documentid\": document_id},\n        UpdateExpression=\"SET conversations = :conversations\",\n        ExpressionAttributeValues={\":conversations\": conversations},\n    )\n\n    conversation = {\"userid\": user_id, \"SessionId\": conversation_id, \"History\": []}\n    memory_table.put_item(Item=conversation)\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": {\n            \"Content-Type\": \"application/json\",\n            \"Access-Control-Allow-Headers\": \"*\",\n            \"Access-Control-Allow-Origin\": \"*\",\n            \"Access-Control-Allow-Methods\": \"*\",\n        },\n        \"body\": json.dumps({\"conversationid\": conversation_id}),\n    }\n"
  },
  {
    "path": "backend/src/add_conversation/requirements.txt",
    "content": "shortuuid==1.0.11"
  },
  {
    "path": "backend/src/delete_document/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/delete_document/main.py",
    "content": "import os, json\nimport boto3\nfrom aws_lambda_powertools import Logger\n\n\nDOCUMENT_TABLE = os.environ[\"DOCUMENT_TABLE\"]\nMEMORY_TABLE = os.environ[\"MEMORY_TABLE\"]\nBUCKET = os.environ[\"BUCKET\"]\n\nddb = boto3.resource(\"dynamodb\")\ndocument_table = ddb.Table(DOCUMENT_TABLE)\nmemory_table = ddb.Table(MEMORY_TABLE)\ns3 = boto3.client(\"s3\")\nlogger = Logger()\n\n\n@logger.inject_lambda_context(log_event=True)\ndef lambda_handler(event, context):\n    user_id = event[\"requestContext\"][\"authorizer\"][\"claims\"][\"sub\"]\n    document_id = event[\"pathParameters\"][\"documentid\"]\n\n    response = document_table.get_item(\n        Key={\"userid\": user_id, \"documentid\": document_id}\n    )\n    document = response[\"Item\"]\n    logger.info({\"document\": document})\n    logger.info(\"Deleting DDB items\")\n    with memory_table.batch_writer() as batch:\n        for item in document[\"conversations\"]:\n            batch.delete_item(Key={\"userid\": user_id, \"SessionId\": item[\"conversationid\"]})\n\n    document_table.delete_item(\n        Key={\"userid\": user_id, \"documentid\": document_id}\n    )\n\n    logger.info(\"Deleting S3 objects\")\n    filename = document[\"filename\"]\n    objects = [{\"Key\": f\"{user_id}/{filename}/{key}\"} for key in [filename, \"index.faiss\", \"index.pkl\"]]\n    response = s3.delete_objects(\n        Bucket=BUCKET,\n        Delete={\n            \"Objects\": objects,\n            \"Quiet\": True,\n        },\n    )\n    logger.info({\"Response\": response})\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": {\n            \"Content-Type\": \"application/json\",\n            \"Access-Control-Allow-Headers\": \"*\",\n            \"Access-Control-Allow-Origin\": \"*\",\n            \"Access-Control-Allow-Methods\": \"*\",\n        },\n        \"body\": json.dumps(\n            {},\n            default=str,\n        ),\n    }\n"
  },
  {
    "path": "backend/src/delete_document/requirements.txt",
    "content": "boto3==1.28.57\nbotocore==1.31.57\n"
  },
  {
    "path": "backend/src/generate_embeddings/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/generate_embeddings/main.py",
    "content": "import os, json\nimport boto3\nfrom aws_lambda_powertools import Logger\nfrom langchain.indexes import VectorstoreIndexCreator\nfrom langchain_aws.embeddings import BedrockEmbeddings\nfrom langchain_community.document_loaders import PyPDFLoader\nfrom langchain_community.vectorstores import FAISS\n\n\nDOCUMENT_TABLE = os.environ[\"DOCUMENT_TABLE\"]\nBUCKET = os.environ[\"BUCKET\"]\nEMBEDDING_MODEL_ID = os.environ[\"EMBEDDING_MODEL_ID\"]\n\ns3 = boto3.client(\"s3\")\nddb = boto3.resource(\"dynamodb\")\ndocument_table = ddb.Table(DOCUMENT_TABLE)\nlogger = Logger()\n\n\ndef set_doc_status(user_id, document_id, status):\n    document_table.update_item(\n        Key={\"userid\": user_id, \"documentid\": document_id},\n        UpdateExpression=\"SET docstatus = :docstatus\",\n        ExpressionAttributeValues={\":docstatus\": status},\n    )\n\n\n@logger.inject_lambda_context(log_event=True)\ndef lambda_handler(event, context):\n    event_body = json.loads(event[\"Records\"][0][\"body\"])\n    document_id = event_body[\"documentid\"]\n    user_id = event_body[\"user\"]\n    key = event_body[\"key\"]\n    file_name_full = key.split(\"/\")[-1]\n\n    set_doc_status(user_id, document_id, \"PROCESSING\")\n\n    s3.download_file(BUCKET, key, f\"/tmp/{file_name_full}\")\n\n    loader = PyPDFLoader(f\"/tmp/{file_name_full}\")\n\n    bedrock_runtime = boto3.client(\n        service_name=\"bedrock-runtime\",\n        region_name=\"us-east-1\",\n    )\n\n    embeddings = BedrockEmbeddings(\n        model_id=EMBEDDING_MODEL_ID,\n        client=bedrock_runtime,\n        region_name=\"us-east-1\",\n    )\n\n    index_creator = VectorstoreIndexCreator(\n        vectorstore_cls=FAISS,\n        embedding=embeddings,\n    )\n\n    index_from_loader = index_creator.from_loaders([loader])\n\n    index_from_loader.vectorstore.save_local(\"/tmp\")\n\n    s3.upload_file(\n        \"/tmp/index.faiss\", BUCKET, f\"{user_id}/{file_name_full}/index.faiss\"\n    )\n    s3.upload_file(\"/tmp/index.pkl\", BUCKET, f\"{user_id}/{file_name_full}/index.pkl\")\n\n    set_doc_status(user_id, document_id, \"READY\")\n"
  },
  {
    "path": "backend/src/generate_embeddings/requirements.txt",
    "content": "boto3\nbotocore\nfaiss-cpu==1.7.4\nlangchain==0.3.21\nlangchain-community==0.3.27\nlangchain-aws==0.2.17\npypdf==3.17.0\nurllib3"
  },
  {
    "path": "backend/src/generate_presigned_url/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/generate_presigned_url/main.py",
    "content": "import os, json\nimport boto3\nfrom botocore.config import Config\nimport shortuuid\nfrom aws_lambda_powertools import Logger\n\n\nBUCKET = os.environ[\"BUCKET\"]\nREGION = os.environ[\"REGION\"]\n\n\ns3 = boto3.client(\n    \"s3\",\n    endpoint_url=f\"https://s3.{REGION}.amazonaws.com\",\n    config=Config(\n        s3={\"addressing_style\": \"virtual\"}, region_name=REGION, signature_version=\"s3v4\"\n    ),\n)\nlogger = Logger()\n\n\ndef s3_key_exists(bucket, key):\n    try:\n        s3.head_object(Bucket=bucket, Key=key)\n        return True\n    except:\n        return False\n\n\n@logger.inject_lambda_context(log_event=True)\ndef lambda_handler(event, context):\n    user_id = event[\"requestContext\"][\"authorizer\"][\"claims\"][\"sub\"]\n    file_name_full = event[\"queryStringParameters\"][\"file_name\"]\n    file_name = file_name_full.split(\".pdf\")[0]\n\n    exists = s3_key_exists(BUCKET, f\"{user_id}/{file_name_full}/{file_name_full}\")\n\n    logger.info(\n        {\n            \"user_id\": user_id,\n            \"file_name_full\": file_name_full,\n            \"file_name\": file_name,\n            \"exists\": exists,\n        }\n    )\n\n    if exists:\n        suffix = shortuuid.ShortUUID().random(length=4)\n        key = f\"{user_id}/{file_name}-{suffix}.pdf/{file_name}-{suffix}.pdf\"\n    else:\n        key = f\"{user_id}/{file_name}.pdf/{file_name}.pdf\"\n\n    presigned_url = s3.generate_presigned_url(\n        ClientMethod=\"put_object\",\n        Params={\n            \"Bucket\": BUCKET,\n            \"Key\": key,\n            \"ContentType\": \"application/pdf\",\n        },\n        ExpiresIn=300,\n        HttpMethod=\"PUT\",\n    )\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": {\n            \"Content-Type\": \"application/json\",\n            \"Access-Control-Allow-Headers\": \"*\",\n            \"Access-Control-Allow-Origin\": \"*\",\n            \"Access-Control-Allow-Methods\": \"*\",\n        },\n        \"body\": json.dumps({\"presignedurl\": presigned_url}),\n    }\n"
  },
  {
    "path": "backend/src/generate_presigned_url/requirements.txt",
    "content": "boto3==1.28.57\nbotocore==1.31.57\nshortuuid==1.0.11"
  },
  {
    "path": "backend/src/generate_response/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/generate_response/main.py",
    "content": "import os\nimport json\nimport boto3\nfrom aws_lambda_powertools import Logger\nfrom langchain.memory import ConversationBufferMemory\nfrom langchain.chains import ConversationalRetrievalChain\nfrom langchain_community.chat_message_histories import DynamoDBChatMessageHistory\nfrom langchain_community.vectorstores import FAISS\nfrom langchain_aws.chat_models import ChatBedrock\nfrom langchain_aws.embeddings import BedrockEmbeddings\n\n\nMEMORY_TABLE = os.environ[\"MEMORY_TABLE\"]\nBUCKET = os.environ[\"BUCKET\"]\nMODEL_ID = os.environ[\"MODEL_ID\"]\nEMBEDDING_MODEL_ID = os.environ[\"EMBEDDING_MODEL_ID\"]\n\ns3 = boto3.client(\"s3\")\nlogger = Logger()\n\n\ndef get_embeddings():\n    bedrock_runtime = boto3.client(\n        service_name=\"bedrock-runtime\",\n        region_name=\"us-east-1\",\n    )\n\n    embeddings = BedrockEmbeddings(\n        model_id=EMBEDDING_MODEL_ID,\n        client=bedrock_runtime,\n        region_name=\"us-east-1\",\n    )\n    return embeddings\n\ndef get_faiss_index(embeddings, user, file_name):\n    s3.download_file(BUCKET, f\"{user}/{file_name}/index.faiss\", \"/tmp/index.faiss\")\n    s3.download_file(BUCKET, f\"{user}/{file_name}/index.pkl\", \"/tmp/index.pkl\")\n    faiss_index = FAISS.load_local(\"/tmp\", embeddings, allow_dangerous_deserialization=True)\n    return faiss_index\n\ndef create_memory(user_id, conversation_id):\n    message_history = DynamoDBChatMessageHistory(\n        table_name=MEMORY_TABLE, session_id=conversation_id, key={\"userid\": user_id, \"SessionId\":conversation_id}\n    )\n\n    memory = ConversationBufferMemory(\n        memory_key=\"chat_history\",\n        chat_memory=message_history,\n        input_key=\"question\",\n        output_key=\"answer\",\n        return_messages=True,\n    )\n    return memory\n\ndef bedrock_chain(faiss_index, memory, human_input, bedrock_runtime):\n\n    chat = ChatBedrock(\n        model_id=MODEL_ID,\n        model_kwargs={'temperature': 0.0}\n    )\n\n    chain = ConversationalRetrievalChain.from_llm(\n        llm=chat,\n        chain_type=\"stuff\",\n        retriever=faiss_index.as_retriever(),\n        memory=memory,\n        return_source_documents=True,\n    )\n\n    response = chain.invoke({\"question\": human_input})\n\n    return response\n\n@logger.inject_lambda_context(log_event=True)\ndef lambda_handler(event, context):\n    event_body = json.loads(event[\"body\"])\n    file_name = event_body[\"fileName\"]\n    human_input = event_body[\"prompt\"]\n    conversation_id = event[\"pathParameters\"][\"conversationid\"]\n    user = event[\"requestContext\"][\"authorizer\"][\"claims\"][\"sub\"]\n\n    embeddings = get_embeddings()\n    faiss_index = get_faiss_index(embeddings, user, file_name)\n    memory = create_memory(user, conversation_id)\n    bedrock_runtime = boto3.client(\n        service_name=\"bedrock-runtime\",\n        region_name=\"us-east-1\",\n    )\n\n    response = bedrock_chain(faiss_index, memory, human_input, bedrock_runtime)\n    if response:\n        print(f\"{MODEL_ID} -\\nPrompt: {human_input}\\n\\nResponse: {response['answer']}\")\n    else:\n        raise ValueError(f\"Unsupported model ID: {MODEL_ID}\")\n\n    logger.info(str(response['answer']))\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": {\n            \"Content-Type\": \"application/json\",\n            \"Access-Control-Allow-Headers\": \"*\",\n            \"Access-Control-Allow-Origin\": \"*\",\n            \"Access-Control-Allow-Methods\": \"*\",\n        },\n        \"body\": json.dumps(response['answer']),\n    }"
  },
  {
    "path": "backend/src/generate_response/requirements.txt",
    "content": "boto3\nbotocore\nfaiss-cpu==1.7.4\nlangchain==0.3.21\nlangchain-community==0.3.27\nlangchain-aws==0.2.17\nurllib3"
  },
  {
    "path": "backend/src/get_all_documents/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/get_all_documents/main.py",
    "content": "import os, json\nimport boto3\nfrom boto3.dynamodb.conditions import Key\nfrom aws_lambda_powertools import Logger\n\n\nDOCUMENT_TABLE = os.environ[\"DOCUMENT_TABLE\"]\n\n\nddb = boto3.resource(\"dynamodb\")\ndocument_table = ddb.Table(DOCUMENT_TABLE)\nlogger = Logger()\n\n\n@logger.inject_lambda_context(log_event=True)\ndef lambda_handler(event, context):\n    user_id = event[\"requestContext\"][\"authorizer\"][\"claims\"][\"sub\"]\n\n    response = document_table.query(KeyConditionExpression=Key(\"userid\").eq(user_id))\n    items = sorted(response[\"Items\"], key=lambda item: item[\"created\"], reverse=True)\n    for item in items:\n        item[\"conversations\"] = sorted(\n            item[\"conversations\"], key=lambda conv: conv[\"created\"], reverse=True\n        )\n    logger.info({\"items\": items})\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": {\n            \"Content-Type\": \"application/json\",\n            \"Access-Control-Allow-Headers\": \"*\",\n            \"Access-Control-Allow-Origin\": \"*\",\n            \"Access-Control-Allow-Methods\": \"*\",\n        },\n        \"body\": json.dumps(items, default=str),\n    }\n"
  },
  {
    "path": "backend/src/get_document/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/get_document/main.py",
    "content": "import os, json\nimport boto3\nfrom boto3.dynamodb.conditions import Key\nfrom aws_lambda_powertools import Logger\n\n\nDOCUMENT_TABLE = os.environ[\"DOCUMENT_TABLE\"]\nMEMORY_TABLE = os.environ[\"MEMORY_TABLE\"]\n\n\nddb = boto3.resource(\"dynamodb\")\ndocument_table = ddb.Table(DOCUMENT_TABLE)\nmemory_table = ddb.Table(MEMORY_TABLE)\nlogger = Logger()\n\n\n@logger.inject_lambda_context(log_event=True)\ndef lambda_handler(event, context):\n    user_id = event[\"requestContext\"][\"authorizer\"][\"claims\"][\"sub\"]\n    document_id = event[\"pathParameters\"][\"documentid\"]\n    conversation_id = event[\"pathParameters\"][\"conversationid\"]\n\n    response = document_table.get_item(\n        Key={\"userid\": user_id, \"documentid\": document_id}\n    )\n    document = response[\"Item\"]\n    document[\"conversations\"] = sorted(\n        document[\"conversations\"], key=lambda conv: conv[\"created\"], reverse=True\n    )\n    logger.info({\"document\": document})\n\n    response = memory_table.get_item(Key={\"userid\": user_id, \"SessionId\": conversation_id})\n    if not \"Item\" in response:\n        return {\n            \"statusCode\": 403\n        }\n    messages = response[\"Item\"][\"History\"]\n    logger.info({\"messages\": messages})\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": {\n            \"Content-Type\": \"application/json\",\n            \"Access-Control-Allow-Headers\": \"*\",\n            \"Access-Control-Allow-Origin\": \"*\",\n            \"Access-Control-Allow-Methods\": \"*\",\n        },\n        \"body\": json.dumps(\n            {\n                \"conversationid\": conversation_id,\n                \"document\": document,\n                \"messages\": messages,\n            },\n            default=str,\n        ),\n    }\n"
  },
  {
    "path": "backend/src/upload_trigger/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/upload_trigger/main.py",
    "content": "import os, json\nfrom datetime import datetime\nimport boto3\nimport PyPDF2\nimport shortuuid\nimport urllib\nfrom aws_lambda_powertools import Logger\n\nDOCUMENT_TABLE = os.environ[\"DOCUMENT_TABLE\"]\nMEMORY_TABLE = os.environ[\"MEMORY_TABLE\"]\nQUEUE = os.environ[\"QUEUE\"]\nBUCKET = os.environ[\"BUCKET\"]\n\n\nddb = boto3.resource(\"dynamodb\")\ndocument_table = ddb.Table(DOCUMENT_TABLE)\nmemory_table = ddb.Table(MEMORY_TABLE)\nsqs = boto3.client(\"sqs\")\ns3 = boto3.client(\"s3\")\nlogger = Logger()\n\n\n@logger.inject_lambda_context(log_event=True)\ndef lambda_handler(event, context):\n    key = urllib.parse.unquote_plus(event[\"Records\"][0][\"s3\"][\"object\"][\"key\"])\n    split = key.split(\"/\")\n    user_id = split[0]\n    file_name = split[1]\n\n    document_id = shortuuid.uuid()\n\n    s3.download_file(BUCKET, key, f\"/tmp/{file_name}\")\n\n    with open(f\"/tmp/{file_name}\", \"rb\") as f:\n        reader = PyPDF2.PdfReader(f)\n        pages = str(len(reader.pages))\n\n    conversation_id = shortuuid.uuid()\n\n    timestamp = datetime.utcnow()\n    timestamp_str = timestamp.strftime(\"%Y-%m-%dT%H:%M:%S.%fZ\")\n\n    document = {\n        \"userid\": user_id,\n        \"documentid\": document_id,\n        \"filename\": file_name,\n        \"created\": timestamp_str,\n        \"pages\": pages,\n        \"filesize\": str(event[\"Records\"][0][\"s3\"][\"object\"][\"size\"]),\n        \"docstatus\": \"UPLOADED\",\n        \"conversations\": [],\n    }\n\n    conversation = {\"conversationid\": conversation_id, \"created\": timestamp_str}\n    document[\"conversations\"].append(conversation)\n\n    document_table.put_item(Item=document)\n\n    conversation = {\"userid\": user_id, \"SessionId\": conversation_id, \"History\": []}\n    memory_table.put_item(Item=conversation)\n\n    message = {\n        \"documentid\": document_id,\n        \"key\": key,\n        \"user\": user_id,\n    }\n    sqs.send_message(QueueUrl=QUEUE, MessageBody=json.dumps(message))\n"
  },
  {
    "path": "backend/src/upload_trigger/requirements.txt",
    "content": "boto3==1.28.57\nbotocore==1.31.57\nPyPDF2==3.0.1\nshortuuid==1.0.11\n"
  },
  {
    "path": "backend/template.yaml",
    "content": "AWSTemplateFormatVersion: \"2010-09-09\"\nTransform: AWS::Serverless-2016-10-31\nDescription: >\n  serverless-pdf-chat\n\n  SAM Template for serverless-pdf-chat\n\nGlobals:\n  Function:\n    Runtime: python3.11\n    Handler: main.lambda_handler\n    Architectures:\n      - arm64\n    Tracing: Active\n    Environment:\n      Variables:\n        LOG_LEVEL: INFO\n    Layers:\n      - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:7\n\nParameters:\n  Frontend:\n    Default: amplify\n    Type: String\n    AllowedValues:\n      - local\n      - amplify\n  Repository:\n    Type: String\n  ModelId:\n    Default: \"anthropic.claude-3-sonnet-20240229-v1:0\"\n    Type: String\n  EmbeddingModelId:\n    Default: \"amazon.titan-embed-text-v2:0\"\n    Type: String\n\nConditions:\n  DeployToAmplifyHosting: !Equals\n    - !Ref Frontend\n    - amplify\n\nResources:\n  DocumentBucket:\n    Type: \"AWS::S3::Bucket\"\n    Properties:\n      BucketName: !Sub \"${AWS::StackName}-${AWS::Region}-${AWS::AccountId}\"\n      CorsConfiguration:\n        CorsRules:\n          - AllowedHeaders:\n              - \"*\"\n            AllowedMethods:\n              - GET\n              - PUT\n              - HEAD\n              - POST\n              - DELETE\n            AllowedOrigins:\n              - \"*\"\n      PublicAccessBlockConfiguration:\n        BlockPublicAcls: true\n        BlockPublicPolicy: true\n        IgnorePublicAcls: true\n        RestrictPublicBuckets: true\n\n  DocumentBucketPolicy:\n    Type: \"AWS::S3::BucketPolicy\"\n    Properties:\n      PolicyDocument:\n        Id: EnforceHttpsPolicy\n        Version: \"2012-10-17\"\n        Statement:\n          - Sid: EnforceHttpsSid\n            Effect: Deny\n            Principal: \"*\"\n            Action: \"s3:*\"\n            Resource:\n              - !Sub \"arn:aws:s3:::${DocumentBucket}/*\"\n              - !Sub \"arn:aws:s3:::${DocumentBucket}\"\n            Condition:\n              Bool:\n                \"aws:SecureTransport\": \"false\"\n      Bucket: !Ref DocumentBucket\n\n  EmbeddingQueue:\n    Type: AWS::SQS::Queue\n    DeletionPolicy: Delete\n    UpdateReplacePolicy: Delete\n    Properties:\n      VisibilityTimeout: 180\n      MessageRetentionPeriod: 3600\n\n  EmbeddingQueuePolicy:\n    Type: AWS::SQS::QueuePolicy\n    Properties:\n      Queues:\n        - !Ref EmbeddingQueue\n      PolicyDocument:\n        Version: \"2012-10-17\"\n        Id: SecureTransportPolicy\n        Statement:\n          - Sid: AllowSecureTransportOnly\n            Effect: Deny\n            Principal: \"*\"\n            Action: \"SQS:*\"\n            Resource: \"*\"\n            Condition:\n              Bool:\n                aws:SecureTransport: false\n\n  DocumentTable:\n    Type: AWS::DynamoDB::Table\n    DeletionPolicy: Delete\n    UpdateReplacePolicy: Delete\n    Properties:\n      KeySchema:\n        - AttributeName: userid\n          KeyType: HASH\n        - AttributeName: documentid\n          KeyType: RANGE\n      AttributeDefinitions:\n        - AttributeName: userid\n          AttributeType: S\n        - AttributeName: documentid\n          AttributeType: S\n      BillingMode: PAY_PER_REQUEST\n\n  MemoryTable:\n    Type: AWS::DynamoDB::Table\n    DeletionPolicy: Delete\n    UpdateReplacePolicy: Delete\n    Properties:\n      KeySchema:\n        - AttributeName: userid\n          KeyType: HASH      \n        - AttributeName: SessionId\n          KeyType: RANGE\n      AttributeDefinitions:\n        - AttributeName: userid\n          AttributeType: S\n        - AttributeName: SessionId\n          AttributeType: S\n      BillingMode: PAY_PER_REQUEST\n\n  CognitoUserPool:\n    Type: AWS::Cognito::UserPool\n    DeletionPolicy: Delete\n    UpdateReplacePolicy: Delete\n    Properties:\n      AutoVerifiedAttributes:\n        - email\n      UsernameAttributes:\n        - email\n      AdminCreateUserConfig:\n        AllowAdminCreateUserOnly: true\n      Policies:\n        PasswordPolicy:\n          MinimumLength: 8\n          RequireLowercase: true\n          RequireNumbers: true\n          RequireSymbols: true\n          RequireUppercase: true\n\n  CognitoUserPoolClient:\n    Type: AWS::Cognito::UserPoolClient\n    Properties:\n      UserPoolId: !Ref CognitoUserPool\n      ClientName: !Ref CognitoUserPool\n      GenerateSecret: false\n\n  Api:\n    Type: AWS::Serverless::Api\n    Properties:\n      StageName: dev\n      Auth:\n        DefaultAuthorizer: CognitoAuthorizer\n        AddDefaultAuthorizerToCorsPreflight: false\n        Authorizers:\n          CognitoAuthorizer:\n            UserPoolArn: !GetAtt CognitoUserPool.Arn\n      Cors:\n        AllowOrigin: \"'*'\"\n        AllowHeaders: \"'*'\"\n        AllowMethods: \"'*'\"\n\n  GeneratePresignedUrlFunction:\n    Type: AWS::Serverless::Function\n    Properties:\n      CodeUri: src/generate_presigned_url/\n      Policies:\n        - S3CrudPolicy:\n            BucketName: !Ref DocumentBucket\n      Environment:\n        Variables:\n          BUCKET: !Ref DocumentBucket\n          REGION: !Sub ${AWS::Region}\n      Events:\n        Root:\n          Type: Api\n          Properties:\n            RestApiId: !Ref Api\n            Path: /generate_presigned_url\n            Method: GET\n\n  UploadTriggerFunction:\n    Type: AWS::Serverless::Function\n    Properties:\n      CodeUri: src/upload_trigger/\n      Policies:\n        - DynamoDBCrudPolicy:\n            TableName: !Ref DocumentTable\n        - DynamoDBCrudPolicy:\n            TableName: !Ref MemoryTable\n        - S3ReadPolicy:\n            BucketName: !Sub \"${AWS::StackName}-${AWS::Region}-${AWS::AccountId}*\"\n        - SQSSendMessagePolicy:\n            QueueName: !GetAtt EmbeddingQueue.QueueName\n      Environment:\n        Variables:\n          DOCUMENT_TABLE: !Ref DocumentTable\n          MEMORY_TABLE: !Ref MemoryTable\n          QUEUE: !GetAtt EmbeddingQueue.QueueName\n          BUCKET: !Sub \"${AWS::StackName}-${AWS::Region}-${AWS::AccountId}\"\n      Events:\n        S3Event:\n          Type: S3\n          Properties:\n            Bucket: !Ref DocumentBucket\n            Events:\n              - s3:ObjectCreated:*\n            Filter:\n              S3Key:\n                Rules:\n                  - Name: suffix\n                    Value: .pdf\n\n  GetDocumentFunction:\n    Type: AWS::Serverless::Function\n    Properties:\n      CodeUri: src/get_document/\n      Policies:\n        - DynamoDBReadPolicy:\n            TableName: !Ref DocumentTable\n        - DynamoDBReadPolicy:\n            TableName: !Ref MemoryTable\n      Environment:\n        Variables:\n          DOCUMENT_TABLE: !Ref DocumentTable\n          MEMORY_TABLE: !Ref MemoryTable\n      Events:\n        Root:\n          Type: Api\n          Properties:\n            RestApiId: !Ref Api\n            Path: /doc/{documentid}/{conversationid}\n            Method: GET\n\n  GetAllDocuments:\n    Type: AWS::Serverless::Function\n    Properties:\n      CodeUri: src/get_all_documents/\n      Policies:\n        - DynamoDBReadPolicy:\n            TableName: !Ref DocumentTable\n      Environment:\n        Variables:\n          DOCUMENT_TABLE: !Ref DocumentTable\n      Events:\n        Root:\n          Type: Api\n          Properties:\n            RestApiId: !Ref Api\n            Path: /doc\n            Method: GET\n\n  AddConversationFunction:\n    Type: AWS::Serverless::Function\n    Properties:\n      CodeUri: src/add_conversation/\n      Policies:\n        - DynamoDBCrudPolicy:\n            TableName: !Ref DocumentTable\n        - DynamoDBCrudPolicy:\n            TableName: !Ref MemoryTable\n      Environment:\n        Variables:\n          DOCUMENT_TABLE: !Ref DocumentTable\n          MEMORY_TABLE: !Ref MemoryTable\n      Events:\n        Root:\n          Type: Api\n          Properties:\n            RestApiId: !Ref Api\n            Path: /doc/{documentid}\n            Method: POST\n\n  GenerateEmbeddingsFunction:\n    Type: AWS::Serverless::Function\n    Properties:\n      CodeUri: src/generate_embeddings/\n      Timeout: 180\n      MemorySize: 2048\n      Policies:\n        - SQSPollerPolicy:\n            QueueName: !GetAtt EmbeddingQueue.QueueName\n        - S3CrudPolicy:\n            BucketName: !Ref DocumentBucket\n        - DynamoDBCrudPolicy:\n            TableName: !Ref DocumentTable\n        - Statement:\n            - Sid: \"BedrockScopedAccess\"\n              Effect: \"Allow\"\n              Action: \"bedrock:InvokeModel\"\n              Resource: !Sub \"arn:aws:bedrock:*::foundation-model/${EmbeddingModelId}\"\n      Environment:\n        Variables:\n          DOCUMENT_TABLE: !Ref DocumentTable\n          BUCKET: !Ref DocumentBucket\n          EMBEDDING_MODEL_ID: !Ref EmbeddingModelId\n      Events:\n        EmbeddingQueueEvent:\n          Type: SQS\n          Properties:\n            Queue: !GetAtt EmbeddingQueue.Arn\n            BatchSize: 1\n\n  GenerateResponseFunction:\n    Type: AWS::Serverless::Function\n    Properties:\n      CodeUri: src/generate_response/\n      Timeout: 30\n      MemorySize: 2048\n      Policies:\n        - DynamoDBCrudPolicy:\n            TableName: !Ref MemoryTable\n        - S3CrudPolicy:\n            BucketName: !Ref DocumentBucket\n        - Statement:\n            - Sid: \"BedrockScopedAccess\"\n              Effect: \"Allow\"\n              Action: \"bedrock:InvokeModel\"\n              Resource:\n                - !Sub \"arn:aws:bedrock:*::foundation-model/${ModelId}\"\n                - !Sub \"arn:aws:bedrock:*::foundation-model/${EmbeddingModelId}\"\n      Environment:\n        Variables:\n          MEMORY_TABLE: !Ref MemoryTable\n          BUCKET: !Ref DocumentBucket\n          MODEL_ID: !Ref ModelId\n          EMBEDDING_MODEL_ID: !Ref EmbeddingModelId\n      Events:\n        Root:\n          Type: Api\n          Properties:\n            RestApiId: !Ref Api\n            Path: /{documentid}/{conversationid}\n            Method: POST\n\n  DeleteDocumentFunction:\n    Type: AWS::Serverless::Function\n    Properties:\n      CodeUri: src/delete_document/\n      Policies:\n        - DynamoDBCrudPolicy:\n            TableName: !Ref DocumentTable\n        - DynamoDBCrudPolicy:\n            TableName: !Ref MemoryTable\n        - S3CrudPolicy:\n            BucketName: !Sub \"${AWS::StackName}-${AWS::Region}-${AWS::AccountId}*\"\n      Environment:\n        Variables:\n          DOCUMENT_TABLE: !Ref DocumentTable\n          MEMORY_TABLE: !Ref MemoryTable\n          BUCKET: !Sub \"${AWS::StackName}-${AWS::Region}-${AWS::AccountId}\"\n      Events:\n        Root:\n          Type: Api\n          Properties:\n            RestApiId: !Ref Api\n            Path: /doc/{documentid}\n            Method: DELETE\n\n  AmplifyApp:\n    Type: AWS::Amplify::App\n    Condition: DeployToAmplifyHosting\n    Properties:\n      Name: !Sub \"${AWS::StackName}-${AWS::Region}-${AWS::AccountId}\"\n      Repository: !Ref Repository\n      BuildSpec: |\n        version: 1\n        applications:\n          - frontend:\n              phases:\n                preBuild:\n                  commands:\n                    - npm ci\n                build:\n                  commands:\n                    - npm run build\n              artifacts:\n                baseDirectory: dist\n                files:\n                  - '**/*'\n              cache:\n                paths:\n                  - node_modules/**/*\n            appRoot: frontend\n      AccessToken: \"{{resolve:secretsmanager:serverless-pdf-chat-github-token}}\"\n      EnvironmentVariables:\n        - Name: AMPLIFY_MONOREPO_APP_ROOT\n          Value: frontend\n        - Name: VITE_REGION\n          Value: !Ref AWS::Region\n        - Name: VITE_API_ENDPOINT\n          Value: !Sub \"https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/dev/\"\n        - Name: VITE_USER_POOL_ID\n          Value: !Ref CognitoUserPool\n        - Name: VITE_USER_POOL_CLIENT_ID\n          Value: !Ref CognitoUserPoolClient\n\n  AmplifyBranch:\n    Type: AWS::Amplify::Branch\n    Condition: DeployToAmplifyHosting\n    Properties:\n      BranchName: main\n      AppId: !GetAtt AmplifyApp.AppId\n      EnableAutoBuild: true\n      Stage: PRODUCTION\n\nOutputs:\n  CognitoUserPool:\n    Value: !Ref CognitoUserPool\n  CognitoUserPoolClient:\n    Value: !Ref CognitoUserPoolClient\n  ApiGatewayBaseUrl:\n    Value: !Sub \"https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/dev/\"\n"
  },
  {
    "path": "frontend/.eslintrc.cjs",
    "content": "module.exports = {\n  env: { browser: true, es2020: true },\n  extends: [\n    'eslint:recommended',\n    'plugin:@typescript-eslint/recommended',\n    'plugin:react-hooks/recommended',\n  ],\n  parser: '@typescript-eslint/parser',\n  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },\n  plugins: ['react-refresh'],\n  rules: {\n    'react-refresh/only-export-components': 'warn',\n  },\n}\n"
  },
  {
    "path": "frontend/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n# Vite\n.env.development"
  },
  {
    "path": "frontend/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>DocChat - Chat with a PDF</title>\n  </head>\n  <body>\n    <div id=\"root\" className=\"flex flex-col min-h-screen\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n\n<script>\n  var global = global || window;\n</script>\n"
  },
  {
    "path": "frontend/package.json",
    "content": "{\n  \"name\": \"frontend\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"lint\": \"eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@aws-amplify/ui-react\": \"^6.1.14\",\n    \"@headlessui/react\": \"^1.7.15\",\n    \"@heroicons/react\": \"^2.0.18\",\n    \"aws-amplify\": \"^6.5.0\",\n    \"date-fns\": \"^2.30.0\",\n    \"filesize\": \"^10.0.7\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^7.5.2\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/typography\": \"^0.5.9\",\n    \"@types/react\": \"^18.0.37\",\n    \"@types/react-dom\": \"^18.0.11\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.59.0\",\n    \"@typescript-eslint/parser\": \"^5.59.0\",\n    \"@vitejs/plugin-react\": \"^4.3.4\",\n    \"autoprefixer\": \"^10.4.14\",\n    \"eslint\": \"^8.38.0\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"eslint-plugin-react-refresh\": \"^0.3.4\",\n    \"postcss\": \"^8.4.24\",\n    \"tailwindcss\": \"^3.3.2\",\n    \"typescript\": \"^5.0.2\",\n    \"vite\": \"^6.4.1\"\n  }\n}\n"
  },
  {
    "path": "frontend/postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "frontend/src/App.tsx",
    "content": "import { Amplify } from \"aws-amplify\";\nimport { fetchAuthSession } from \"aws-amplify/auth\";\nimport { withAuthenticator } from \"@aws-amplify/ui-react\";\nimport { createBrowserRouter, RouterProvider } from \"react-router-dom\";\nimport \"./index.css\";\nimport Layout from \"./routes/layout\";\nimport Documents from \"./routes/documents\";\nimport Chat from \"./routes/chat\";\n\nAmplify.configure({\n  Auth: {\n    Cognito: {\n      userPoolId: import.meta.env.VITE_USER_POOL_ID,\n      userPoolClientId: import.meta.env.VITE_USER_POOL_CLIENT_ID,\n    },\n  },\n  API: {\n    REST: {\n      \"serverless-pdf-chat\": {\n        endpoint: import.meta.env.VITE_API_ENDPOINT,\n        region: import.meta.env.VITE_API_REGION,\n      },\n    },\n  }}, {\n  API: {\n    REST: {\n      headers: async () => {\n        const tokens = (await fetchAuthSession()).tokens;\n        const jwt = tokens?.idToken?.toString();\n          return {\n            \"authorization\": `Bearer ${jwt}`\n          };\n      }\n    }\n  }\n});\n\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: <Layout />,\n    children: [\n      {\n        index: true,\n        Component: Documents,\n      },\n      {\n        path: \"/doc/:documentid/:conversationid\",\n        Component: Chat,\n      },\n    ],\n  },\n]);\n\nfunction App() {\n  return <RouterProvider router={router} />;\n}\n\nexport default withAuthenticator(App, { hideSignUp: true });\n"
  },
  {
    "path": "frontend/src/common/types.ts",
    "content": "export interface Document {\n  documentid: string;\n  userid: string;\n  filename: string;\n  filesize: string;\n  docstatus: string;\n  created: string;\n  pages: string;\n  conversations: {\n    conversationid: string;\n    created: string;\n  }[];\n}\n\nexport interface Conversation {\n  conversationid: string;\n  document: Document;\n  messages: {\n    type: string;\n    data: {\n      content: string;\n      example: boolean;\n      additional_kwargs: {};\n    };\n  }[];\n}\n"
  },
  {
    "path": "frontend/src/common/utilities.ts",
    "content": "import { format } from \"date-fns\";\n\nexport function getDateTime(date: string): string {\n  return format(new Date(date), \"MMMM d, yyyy - H:mm\");\n}\n"
  },
  {
    "path": "frontend/src/components/ChatMessages.tsx",
    "content": "import { PaperAirplaneIcon } from \"@heroicons/react/24/outline\";\nimport Loading from \"../../public/loading-dots.svg\";\nimport { Conversation } from \"../common/types\";\n\ninterface ChatMessagesProps {\n  conversation: Conversation;\n  messageStatus: string;\n  handlePromptChange: (event: React.ChangeEvent<HTMLInputElement>) => void;\n  handleKeyPress: (event: React.KeyboardEvent<HTMLInputElement>) => void;\n  prompt: string;\n  submitMessage: () => Promise<void>;\n}\n\nconst ChatMessages: React.FC<ChatMessagesProps> = ({\n  prompt,\n  conversation,\n  messageStatus,\n  submitMessage,\n  handlePromptChange,\n  handleKeyPress,\n}) => {\n  return (\n    <div className=\"flex flex-col justify-between h-full overflow-y-auto col-span-8 p-5 border-l border-gray-200\">\n      <div className=\"pb-5\">\n        <div className=\"grid gap-5\">\n          {conversation.messages.map((message, i) => (\n            <div\n              className={`${\n                message.type === \"ai\"\n                  ? \"justify-self-start w-fit rounded border border-gray-100 px-5 py-3.5 text-gray-800\"\n                  : \"\" +\n                    \"justify-self-end w-fit bg-slate-100 rounded border border-gray-100 px-5 py-3.5 text-gray-800\"\n              }`}\n              key={i}\n            >\n              <div className=\"prose\">\n                <p>{message.data.content}</p>\n              </div>\n            </div>\n          ))}\n          {messageStatus === \"loading\" && (\n            <div className=\"justify-self-start w-fit rounded border border-gray-100 px-5 py-3.5 text-gray-800\">\n              <img src={Loading} width={40} className=\"py-2 mx-2\" />\n            </div>\n          )}\n        </div>\n      </div>\n\n      <div className=\"relative w-full\">\n        <div className=\"relative\">\n          <input\n            disabled={messageStatus === \"loading\"}\n            type=\"text\"\n            id=\"prompt\"\n            value={prompt}\n            onChange={handlePromptChange}\n            onKeyDown={handleKeyPress}\n            className={\n              messageStatus === \"loading\"\n                ? \"block w-full p-4 pl-4 text-sm text-gray-500 border border-gray-200 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500\"\n                : \"block w-full p-4 pl-4 text-sm text-gray-900 border border-gray-200 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500\"\n            }\n            placeholder={\n              \"Ask \" + conversation.document.filename + \" anything...\"\n            }\n          />\n          {messageStatus === \"idle\" && (\n            <button\n              type=\"submit\"\n              className=\"text-gray-700 absolute right-2 bottom-2 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2\"\n              onClick={submitMessage}\n            >\n              <PaperAirplaneIcon className=\"w-6 h-6\" />\n            </button>\n          )}\n          {messageStatus === \"loading\" && (\n            <button\n              disabled\n              type=\"submit\"\n              className=\"text-gray-700 absolute right-2 bottom-2 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2\"\n            >\n              <svg\n                aria-hidden=\"true\"\n                role=\"status\"\n                className=\"inline w-5 h-5 text-gray-200 animate-spin dark:text-gray-600\"\n                viewBox=\"0 0 100 101\"\n                fill=\"none\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n              >\n                <path\n                  d=\"M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z\"\n                  fill=\"#4C1D95\"\n                />\n              </svg>\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default ChatMessages;\n"
  },
  {
    "path": "frontend/src/components/ChatSidebar.tsx",
    "content": "import DocumentDetail from \"./DocumentDetail\";\nimport { Conversation } from \"../common/types\";\nimport { getDateTime } from \"../common/utilities\";\nimport { Params } from \"react-router-dom\";\nimport {\n  ChatBubbleLeftRightIcon,\n  PlusCircleIcon,\n} from \"@heroicons/react/24/outline\";\n\ninterface ChatSidebarProps {\n  conversation: Conversation;\n  params: Params;\n  addConversation: () => Promise<void>;\n  switchConversation: (e: React.MouseEvent<HTMLButtonElement>) => void;\n  conversationListStatus: \"idle\" | \"loading\";\n}\n\nconst ChatSidebar: React.FC<ChatSidebarProps> = ({\n  conversation,\n  params,\n  addConversation,\n  switchConversation,\n  conversationListStatus,\n}) => {\n  return (\n    <div className=\"col-span-4 h-full\">\n      <div className=\"bg-gray-100 p-5\">\n        <DocumentDetail document={conversation.document} />\n      </div>\n      <div className=\"px-3 pt-3 pb-5\">\n        {conversationListStatus === \"idle\" && (\n          <button\n            onClick={addConversation}\n            className=\"bg-gray-50 w-full inline-flex items-center px-4 py-2.5 border border-gray-100 rounded hover:bg-gray-200\"\n          >\n            <PlusCircleIcon className=\"w-4 h-4 mr-2\" />\n            Start a new conversation\n          </button>\n        )}\n        {conversationListStatus === \"loading\" && (\n          <button\n            disabled\n            onClick={addConversation}\n            className=\"bg-gray-50 w-full inline-flex items-center px-4 py-2.5 border border-gray-100 rounded hover:bg-gray-200\"\n          >\n            <svg\n              aria-hidden=\"true\"\n              role=\"status\"\n              className=\"inline w-4 h-4 mr-2 text-gray-200 animate-spin\"\n              viewBox=\"0 0 100 101\"\n              fill=\"none\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n            >\n              <path\n                d=\"M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z\"\n                fill=\"currentColor\"\n              />\n              <path\n                d=\"M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z\"\n                fill=\"#4C1D95\"\n              />\n            </svg>\n            Start a new conversation\n          </button>\n        )}\n        {conversation &&\n          conversation.document.conversations.map((conversation, i) => (\n            <div key={i}>\n              {params.conversationid === conversation.conversationid && (\n                <button\n                  disabled={\n                    params.conversationid === conversation.conversationid\n                  }\n                  className=\"bg-gray-500 text-white w-full inline-flex items-center mt-2 px-4 py-2.5 border border-gray-100 rounded\"\n                >\n                  <ChatBubbleLeftRightIcon className=\"w-4 h-4 mr-2\" />\n                  {getDateTime(conversation.created)}\n                </button>\n              )}\n              {params.conversationid !== conversation.conversationid && (\n                <button\n                  id={conversation.conversationid}\n                  onClick={switchConversation}\n                  disabled={\n                    params.conversationid === conversation.conversationid\n                  }\n                  className=\"bg-gray-50 w-full inline-flex items-center mt-2 px-4 py-2.5 border border-gray-100 rounded hover:bg-gray-200\"\n                >\n                  <ChatBubbleLeftRightIcon className=\"w-4 h-4 mr-2\" />\n                  {getDateTime(conversation.created)}\n                </button>\n              )}\n            </div>\n          ))}\n      </div>\n    </div>\n  );\n};\n\nexport default ChatSidebar;\n"
  },
  {
    "path": "frontend/src/components/DocumentDetail.tsx",
    "content": "import { Document } from \"../common/types\";\nimport { getDateTime } from \"../common/utilities\";\nimport { filesize } from \"filesize\";\nimport {\n  DocumentIcon,\n  CircleStackIcon,\n  ClockIcon,\n  CheckCircleIcon,\n  CloudIcon,\n  CogIcon,\n  TrashIcon,\n} from \"@heroicons/react/24/outline\";\nimport { del } from \"aws-amplify/api\";\nimport {useNavigate} from \"react-router-dom\";\nimport {useState} from \"react\";\n\ninterface DocumentDetailProps {\n  document: Document;\n  onDocumentDeleted?: (document?: Document) => void;\n}\n\nconst DocumentDetail: React.FC<DocumentDetailProps> = ({document, onDocumentDeleted}) => {\n  const navigate = useNavigate();\n  const [deleteStatus, setDeleteStatus] = useState<string>(\"idle\");\n\n  const deleteDocument = async (event: React.MouseEvent<HTMLButtonElement>) => {\n    event.preventDefault();\n    setDeleteStatus(\"deleting\");\n    await del({\n      apiName: \"serverless-pdf-chat\",\n      path: `doc/${document.documentid}`,\n    }).response;\n    setDeleteStatus(\"idle\");\n    if (onDocumentDeleted) onDocumentDeleted(document);\n    else navigate(`/`);\n  };\n\n  return (\n    <>\n      <h3 className=\"text-center mb-3 text-lg font-bold tracking-tight text-gray-900\">\n        {document.filename}\n      </h3>\n      <div className=\"flex flex-col space-y-2\">\n        <div className=\"inline-flex items-center\">\n          <DocumentIcon className=\"w-4 h-4 mr-2\" />\n          {document.pages} pages\n        </div>\n        <div className=\"inline-flex items-center\">\n          <CircleStackIcon className=\"w-4 h-4 mr-2\" />\n          {filesize(Number(document.filesize)).toString()}\n        </div>\n        <div className=\"inline-flex items-center\">\n          <ClockIcon className=\"w-4 h-4 mr-2\" />\n          {getDateTime(document.created)}\n        </div>\n        {document.docstatus === \"UPLOADED\" && (\n          <div className=\"flex flex-row justify-center pt-4\">\n            <span className=\"inline-flex items-center self-start bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded\">\n              <CloudIcon className=\"w-4 h-4 mr-1\" />\n              Awaiting processing\n            </span>\n          </div>\n        )}\n        {document.docstatus === \"PROCESSING\" && (\n          <div className=\"flex flex-row justify-center pt-4\">\n            <span className=\"inline-flex items-center self-start bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded\">\n              <CogIcon className=\"w-4 h-4 mr-1 animate-spin\" />\n              Processing document\n            </span>\n          </div>\n        )}\n        {document.docstatus === \"READY\" && (\n          <div className=\"flex flex-row pt-4\">\n            <div className=\"flex flex-row justify-center flex-grow pt-2\">\n              <span className=\"inline-flex items-center self-start bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded\">\n                <CheckCircleIcon className=\"w-4 h-4 mr-1\" />\n                Ready to chat\n              </span>\n            </div>\n            <div className=\"flex flex-row\">\n              <button\n                onClick={deleteDocument}\n                className=\"text-gray-700 hover:bg-gray-700 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg p-2\"\n              >\n                <TrashIcon className={`\"w-4 h-4 mr-1 ${deleteStatus === \"deleting\" ? \"animate-spin\" : \"\"}`}/>\n              </button>\n            </div>\n          </div>\n        )}\n      </div>\n    </>\n  );\n};\n\nexport default DocumentDetail;\n"
  },
  {
    "path": "frontend/src/components/DocumentList.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { get } from \"aws-amplify/api\";\nimport { Link } from \"react-router-dom\";\nimport DocumentDetail from \"./DocumentDetail\";\nimport { ArrowPathRoundedSquareIcon } from \"@heroicons/react/24/outline\";\nimport { Document } from \"../common/types\";\nimport Loading from \"../../public/loading-grid.svg\";\n\nconst DocumentList: React.FC = () => {\n  const [documents, setDocuments] = useState<Document[]>([]);\n  const [listStatus, setListStatus] = useState<string>(\"idle\");\n\n  const fetchData = async () => {\n    setListStatus(\"loading\");\n    const response = await get({\n      apiName: \"serverless-pdf-chat\", path:\"doc\"\n    }).response;\n    const docs = await response.body.json() as unknown as Document[]\n    setListStatus(\"idle\");\n    setDocuments(docs);\n  };\n\n  useEffect(() => {\n    fetchData();\n  }, []);\n\n  return (\n    <div>\n      <div className=\"flex justify-between pt-6 pb-4\">\n        <h2 className=\"text-2xl font-bold\">My documents</h2>\n        <button\n          onClick={fetchData}\n          type=\"button\"\n          className=\"text-gray-700 border border-gray-700 hover:bg-gray-700 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm p-2 text-center inline-flex items-center\"\n        >\n          <ArrowPathRoundedSquareIcon\n            className={`w-5 h-5 ${\n              listStatus === \"loading\" ? \"animate-spin\" : \"\"\n            }`}\n          />\n        </button>\n      </div>\n      <div className=\"grid grid-cols-3 gap-4\">\n        {documents &&\n          documents.length > 0 &&\n          documents.map((document: Document) => (\n            <Link\n              to={`/doc/${document.documentid}/${document.conversations[0].conversationid}/`}\n              key={document.documentid}\n              className=\"block p-6 bg-white border border-gray-200 rounded hover:bg-gray-100\"\n            >\n              <DocumentDetail document={document} onDocumentDeleted={fetchData}/>\n            </Link>\n          ))}\n      </div>\n      {listStatus === \"idle\" && documents.length === 0 && (\n        <div className=\"flex flex-col items-center mt-4\">\n          <p className=\"font-bold text-lg\">There's nothing here yet...</p>\n          <p className=\"mt-1\">Upload your first document to get started!</p>\n        </div>\n      )}\n      {listStatus === \"loading\" && documents.length === 0 && (\n        <div className=\"flex flex-col items-center mt-4\">\n          <img src={Loading} width={40} />\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default DocumentList;\n"
  },
  {
    "path": "frontend/src/components/DocumentUploader.tsx",
    "content": "import { ChangeEvent, useState, useEffect } from \"react\";\nimport { get } from \"aws-amplify/api\";\nimport { filesize } from \"filesize\";\nimport {\n  DocumentIcon,\n  CheckCircleIcon,\n  CloudArrowUpIcon,\n  XCircleIcon,\n  ArrowLeftCircleIcon,\n} from \"@heroicons/react/24/outline\";\n\ninterface DocumentUploaderProps {\n  onDocumentUploaded?:() => void\n}\nconst DocumentUploader: React.FC<DocumentUploaderProps> = ({onDocumentUploaded}) => {\n  const [inputStatus, setInputStatus] = useState<string>(\"idle\");\n  const [buttonStatus, setButtonStatus] = useState<string>(\"ready\");\n  const [selectedFile, setSelectedFile] = useState<File | null>(null);\n\n  useEffect(() => {\n    if (selectedFile) {\n      if (selectedFile.type === \"application/pdf\") {\n        setInputStatus(\"valid\");\n      } else {\n        setSelectedFile(null);\n      }\n    }\n  }, [selectedFile]);\n\n  const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0];\n    setSelectedFile(file || null);\n  };\n\n  const uploadFile = async () => {\n    if(selectedFile) {\n      setButtonStatus(\"uploading\");\n      const response = await get({\n        apiName: \"serverless-pdf-chat\",\n        path: \"generate_presigned_url\",\n        options: {\n          headers: { \"Content-Type\": \"application/json\" },\n          queryParams: {\n            \"file_name\": selectedFile?.name\n          }\n        },\n      }).response\n      const presignedUrl = await response.body.json() as { presignedurl: string }\n      fetch(presignedUrl?.presignedurl, {\n        method: \"PUT\",\n        body: selectedFile,\n        headers: { \"Content-Type\": \"application/pdf\" },\n      }).then(() => {\n        setButtonStatus(\"success\");\n        if (onDocumentUploaded) onDocumentUploaded();\n      });\n    }\n  };\n\n  const resetInput = () => {\n    setSelectedFile(null);\n    setInputStatus(\"idle\");\n    setButtonStatus(\"ready\");\n  };\n\n  return (\n    <div>\n      <h2 className=\"text-2xl font-bold pb-4\">Add document</h2>\n      {inputStatus === \"idle\" && (\n        <div className=\"flex items-center justify-center w-full\">\n          <label\n            htmlFor=\"dropzone-file\"\n            className=\"flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100\"\n          >\n            <div className=\"flex flex-col items-center justify-center pt-5 pb-6\">\n              <CloudArrowUpIcon className=\"w-12 h-12 mb-3 text-gray-400\" />\n\n              <p className=\"mb-2 text-sm text-gray-500\">\n                <span className=\"font-semibold\">Click to upload</span> your\n                document\n              </p>\n              <p className=\"text-xs text-gray-500\">Only .pdf accepted</p>\n            </div>\n\n            <input\n              onChange={handleFileChange}\n              id=\"dropzone-file\"\n              type=\"file\"\n              className=\"hidden\"\n            />\n          </label>\n        </div>\n      )}\n      {inputStatus === \"valid\" && (\n        <div className=\"flex items-center justify-center w-full\">\n          <div className=\"flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg bg-gray-50\">\n            <>\n              <div className=\"flex flex-row items-center mb-5\">\n                <DocumentIcon className=\"w-14 h-14 text-gray-400\" />\n                <div className=\"flex flex-col ml-2\">\n                  <p className=\"font-bold mb-1\">{selectedFile?.name}</p>\n                  <p>\n                    {filesize(selectedFile ? selectedFile.size : 0).toString()}\n                  </p>\n                </div>\n              </div>\n              <div className=\"flex flex-row items-center\">\n                {buttonStatus === \"ready\" && (\n                  <button\n                    onClick={resetInput}\n                    type=\"button\"\n                    className=\"inline-flex items-center text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg px-3 py-2 text-sm mr-2 mb-2 \"\n                  >\n                    <XCircleIcon className=\"w-5 h-5 mr-1.5\" />\n                    Cancel\n                  </button>\n                )}\n                {buttonStatus === \"uploading\" && (\n                  <button\n                    disabled\n                    onClick={resetInput}\n                    type=\"button\"\n                    className=\"inline-flex items-center text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg px-3 py-2 text-sm mr-2 mb-2 \"\n                  >\n                    <XCircleIcon className=\"w-5 h-5 mr-1.5\" />\n                    Cancel\n                  </button>\n                )}\n                {buttonStatus === \"success\" && (\n                  <button\n                    onClick={resetInput}\n                    type=\"button\"\n                    className=\"inline-flex items-center text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg px-3 py-2 text-sm mr-2 mb-2 \"\n                  >\n                    <ArrowLeftCircleIcon className=\"w-5 h-5 mr-1.5\" />\n                    Upload another document\n                  </button>\n                )}\n                {buttonStatus === \"ready\" && (\n                  <button\n                    onClick={uploadFile}\n                    type=\"button\"\n                    className=\"inline-flex items-center bg-violet-900 text-white border border-gray-300 focus:outline-none hover:bg-violet-700 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg px-3 py-2 text-sm mr-2 mb-2 \"\n                  >\n                    <CloudArrowUpIcon className=\"w-5 h-5 mr-1.5\" />\n                    Upload document\n                  </button>\n                )}\n                {buttonStatus === \"uploading\" && (\n                  <button\n                    disabled\n                    onClick={uploadFile}\n                    type=\"button\"\n                    className=\"inline-flex items-center bg-violet-900 text-white border border-gray-300 focus:outline-none hover:bg-violet-700 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg px-3 py-2 text-sm mr-2 mb-2 \"\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      role=\"status\"\n                      className=\"inline w-4 h-4 mr-3 text-white animate-spin\"\n                      viewBox=\"0 0 100 101\"\n                      fill=\"none\"\n                      xmlns=\"http://www.w3.org/2000/svg\"\n                    >\n                      <path\n                        d=\"M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z\"\n                        fill=\"#E5E7EB\"\n                      />\n                      <path\n                        d=\"M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z\"\n                        fill=\"currentColor\"\n                      />\n                    </svg>\n                    Uploading...\n                  </button>\n                )}\n                {buttonStatus === \"success\" && (\n                  <button\n                    disabled\n                    onClick={uploadFile}\n                    type=\"button\"\n                    className=\"inline-flex items-center bg-violet-900 text-white border border-gray-300 focus:outline-none hover:bg-violet-700 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg px-3 py-2 text-sm mr-2 mb-2 \"\n                  >\n                    <CheckCircleIcon className=\"w-5 h-5 mr-1.5\" />\n                    Upload successful!\n                  </button>\n                )}\n              </div>\n            </>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default DocumentUploader;\n"
  },
  {
    "path": "frontend/src/components/Footer.tsx",
    "content": "import { CloudIcon } from \"@heroicons/react/24/outline\";\nimport GitHub from \"../../public/github.svg\";\n\nconst Footer: React.FC = () => {\n  return (\n    <div className=\"bg-gray-100 mt-auto\">\n      <footer className=\"container\">\n        <div className=\" flex flex-row justify-between py-3 text-sm\">\n          <div className=\"inline-flex items-center\">\n            <CloudIcon className=\"w-5 h-5 mr-1.5\" />\n            Powered by Amazon Web Services\n          </div>\n          <div className=\"inline-flex items-center hover:underline underline-offset-2\">\n            <img\n              src={GitHub}\n              alt=\"React Logo\"\n              width={20}\n              className=\"mr-1.5 py-2 mx-2\"\n            />\n            <a href=\"https://github.com/aws-samples/serverless-pdf-chat\">\n              Source code on GitHub\n            </a>\n          </div>\n        </div>\n      </footer>\n    </div>\n  );\n};\n\nexport default Footer;\n"
  },
  {
    "path": "frontend/src/components/Navigation.tsx",
    "content": "import { Link } from \"react-router-dom\";\nimport { Menu } from \"@headlessui/react\";\nimport {\n  ArrowLeftOnRectangleIcon,\n  ChevronDownIcon,\n} from \"@heroicons/react/24/outline\";\nimport { ChatBubbleLeftRightIcon } from \"@heroicons/react/24/solid\";\n\ninterface NavigationProps {\n  userInfo: any;\n  handleSignOutClick: (\n    event: React.MouseEvent<HTMLButtonElement>\n  ) => Promise<void>;\n}\n\nconst Navigation: React.FC<NavigationProps> = ({\n  userInfo,\n  handleSignOutClick,\n}: NavigationProps) => {\n  return (\n    <nav className=\"bg-violet-900\">\n      <div className=\"container flex flex-wrap items-center justify-between py-3\">\n        <Link\n          to=\"/\"\n          className=\"inline-flex items-center self-center text-2xl font-semibold whitespace-nowrap text-white\"\n        >\n          <ChatBubbleLeftRightIcon className=\"w-6 h-6 mr-1.5\" />\n          DocChat\n        </Link>\n        <div className=\"absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0\">\n          <div className=\"relative ml-3\">\n            <Menu>\n              <Menu.Button className=\"text-center inline-flex items-center text-white text-sm underline-offset-2 hover:underline\">\n                {userInfo?.attributes?.email}\n                <ChevronDownIcon className=\"w-3 h-3 ml-1 text-white\" />\n              </Menu.Button>\n              <Menu.Items className=\"absolute right-0 z-10 mt-2 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none\">\n                <div className=\"px-1 py-1 \">\n                  <Menu.Item>\n                    <button\n                      onClick={handleSignOutClick}\n                      className=\"group w-full inline-flex items-center rounded-md px-2 py-2 text-sm underline-offset-2 hover:underline\"\n                    >\n                      <ArrowLeftOnRectangleIcon className=\"w-4 h-4 mr-1\" />\n                      Sign Out\n                    </button>\n                  </Menu.Item>\n                </div>\n              </Menu.Items>\n            </Menu>\n          </div>\n        </div>\n      </div>\n    </nav>\n  );\n};\n\nexport default Navigation;\n"
  },
  {
    "path": "frontend/src/index.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "frontend/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport App from \"./App\";\nimport \"@aws-amplify/ui-react/styles.css\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);\n"
  },
  {
    "path": "frontend/src/routes/chat.tsx",
    "content": "import React, { useState, useEffect, KeyboardEvent } from \"react\";\nimport { useParams, useNavigate } from \"react-router-dom\";\nimport { get, post } from \"aws-amplify/api\";\nimport { Conversation } from \"../common/types\";\nimport ChatSidebar from \"../components/ChatSidebar\";\nimport ChatMessages from \"../components/ChatMessages\";\nimport LoadingGrid from \"../../public/loading-grid.svg\";\n\nconst Document: React.FC = () => {\n  const params = useParams();\n  const navigate = useNavigate();\n\n  const [conversation, setConversation] = useState<Conversation | null>(null);\n  const [loading, setLoading] = React.useState<string>(\"idle\");\n  const [messageStatus, setMessageStatus] = useState<string>(\"idle\");\n  const [conversationListStatus, setConversationListStatus] = useState<\n    \"idle\" | \"loading\"\n  >(\"idle\");\n  const [prompt, setPrompt] = useState(\"\");\n\n  const fetchData = async (conversationid = params.conversationid) => {\n    setLoading(\"loading\");\n    const response = await get({\n      apiName: \"serverless-pdf-chat\",\n      path: `doc/${params.documentid}/${conversationid}`\n    }).response\n    const conversation = await response.body.json() as unknown as Conversation\n    setConversation(conversation);\n    setLoading(\"idle\");\n    console.log(\"Foo\")\n  };\n\n  useEffect(() => {\n    fetchData();\n  }, []);\n\n  const handlePromptChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    setPrompt(event.target.value);\n  };\n\n  const addConversation = async () => {\n    setConversationListStatus(\"loading\");\n    const response = await post({\n      apiName: \"serverless-pdf-chat\",\n      path: `doc/${params.documentid}`\n    }).response;\n    const newConversation = await response.body.json() as unknown as Conversation;\n    fetchData(newConversation.conversationid);\n    navigate(`/doc/${params.documentid}/${newConversation.conversationid}`);\n    setConversationListStatus(\"idle\");\n  };\n\n  const switchConversation = (e: React.MouseEvent<HTMLButtonElement>) => {\n    const targetButton = e.target as HTMLButtonElement;\n    navigate(`/doc/${params.documentid}/${targetButton.id}`);\n    fetchData(targetButton.id);\n  };\n\n  const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key == \"Enter\") {\n      submitMessage();\n    }\n  };\n\n  const submitMessage = async () => {\n    setMessageStatus(\"loading\");\n\n    if (conversation !== null) {\n      const previewMessage = {\n        type: \"text\",\n        data: {\n          content: prompt,\n          additional_kwargs: {},\n          example: false,\n        },\n      };\n\n      const updatedConversation = {\n        ...conversation,\n        messages: [...conversation.messages, previewMessage],\n      };\n\n      setConversation(updatedConversation);\n\n\n      await post({\n        apiName: \"serverless-pdf-chat\",\n        path: `${conversation?.document.documentid}/${conversation?.conversationid}`,\n        options: {\n          body: {\n            fileName: conversation?.document.filename,\n            prompt: prompt,\n          }\n        }\n      }).response;\n      setPrompt(\"\");\n      fetchData(conversation?.conversationid);\n      setMessageStatus(\"idle\");\n    }\n  };\n\n  return (\n    <div className=\"\">\n      {loading === \"loading\" && !conversation && (\n        <div className=\"flex flex-col items-center mt-6\">\n          <img src={LoadingGrid} width={40} />\n        </div>\n      )}\n      {conversation && (\n        <div className=\"grid grid-cols-12 border border-gray-200 rounded-lg\">\n          <ChatSidebar\n            conversation={conversation}\n            params={params}\n            addConversation={addConversation}\n            switchConversation={switchConversation}\n            conversationListStatus={conversationListStatus}\n          />\n          <ChatMessages\n            prompt={prompt}\n            conversation={conversation}\n            messageStatus={messageStatus}\n            submitMessage={submitMessage}\n            handleKeyPress={handleKeyPress}\n            handlePromptChange={handlePromptChange}\n          />\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default Document;\n"
  },
  {
    "path": "frontend/src/routes/documents.tsx",
    "content": "import React, {useState} from \"react\";\nimport DocumentUploader from \"../components/DocumentUploader\";\nimport DocumentList from \"../components/DocumentList\";\n\nconst Documents: React.FC = () => {\n  const [documentListKey, setDocumentListKey] = useState(1);\n  const reloadDocuments = () => {\n    setTimeout(() =>setDocumentListKey(Math.random()), 1000);\n  }\n\n\n  return (\n    <>\n      <DocumentUploader onDocumentUploaded={reloadDocuments}/>\n      <DocumentList key={documentListKey}/>\n    </>\n  );\n};\n\nexport default Documents;\n"
  },
  {
    "path": "frontend/src/routes/layout.tsx",
    "content": "import { Outlet } from \"react-router-dom\";\nimport { useEffect, useState } from \"react\";\nimport { signOut, fetchUserAttributes } from \"aws-amplify/auth\";\nimport Navigation from \"../components/Navigation\";\nimport Footer from \"../components/Footer\";\n\nconst Layout: React.FC = () => {\n  const [userInfo, setUserInfo] = useState<any>(null);\n\n  useEffect(() => {\n    (async () => {\n      const attributes = await fetchUserAttributes();\n      setUserInfo({attributes})\n    })();\n  }, []);\n\n  const handleSignOutClick = async (\n    event: React.MouseEvent<HTMLButtonElement>\n  ) => {\n    event.preventDefault();\n    await signOut();\n  };\n\n  return (\n    <div className=\"flex flex-col min-h-screen\">\n      <div>\n        <Navigation\n          userInfo={userInfo}\n          handleSignOutClick={handleSignOutClick}\n        />\n        <div className=\"container mt-6 mb-6\">\n          <Outlet />\n        </div>\n      </div>\n      <Footer />\n    </div>\n  );\n};\n\nexport default Layout;\n"
  },
  {
    "path": "frontend/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "frontend/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx}\"],\n  theme: {\n    extend: {},\n    container: {\n      padding: \"7rem\",\n      center: true,\n    },\n  },\n  plugins: [require(\"@tailwindcss/typography\")],\n};\n"
  },
  {
    "path": "frontend/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"src\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "frontend/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "frontend/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n});\n"
  }
]