From bc142347bbb0c96d174cb36764c81318cf1efea3 Mon Sep 17 00:00:00 2001 From: gbiagini97 Date: Thu, 7 Nov 2024 12:35:08 +0100 Subject: [PATCH 1/2] feat: new patterns for appsync-events --- appsync-events-bedrock-cdk/.gitignore | 8 + appsync-events-bedrock-cdk/.npmignore | 6 + appsync-events-bedrock-cdk/README.md | 94 +++++++ .../bin/appsync-events-bedrock-cdk.ts | 21 ++ appsync-events-bedrock-cdk/cdk-outputs.json | 10 + appsync-events-bedrock-cdk/cdk.json | 80 ++++++ .../demo-app/.gitignore | 24 ++ appsync-events-bedrock-cdk/demo-app/README.md | 1 + .../demo-app/index.html | 13 + .../demo-app/package.json | 34 +++ .../demo-app/public/vite.svg | 1 + .../demo-app/src/App.css | 42 +++ .../demo-app/src/App.tsx | 13 + .../demo-app/src/ChatComponent.tsx | 253 ++++++++++++++++++ .../demo-app/src/assets/react.svg | 1 + .../demo-app/src/index.css | 68 +++++ .../demo-app/src/main.tsx | 10 + .../demo-app/tsconfig.app.json | 26 ++ .../demo-app/tsconfig.json | 25 ++ .../demo-app/tsconfig.node.json | 10 + .../demo-app/vite.config.ts | 7 + appsync-events-bedrock-cdk/diagram.png | Bin 0 -> 239192 bytes .../example-pattern.json | 60 +++++ appsync-events-bedrock-cdk/jest.config.cjs | 8 + .../lib/appsync-events-bedrock-cdk-stack.ts | 123 +++++++++ .../lib/events-api-cfn.yaml | 40 +++ appsync-events-bedrock-cdk/package.json | 40 +++ appsync-events-bedrock-cdk/src/chat.ts | 113 ++++++++ appsync-events-bedrock-cdk/step1.png | Bin 0 -> 350967 bytes appsync-events-bedrock-cdk/step2.png | Bin 0 -> 401298 bytes appsync-events-bedrock-cdk/step3.png | Bin 0 -> 504195 bytes .../test/appsync-events-bedrock-cdk.test.ts | 72 +++++ appsync-events-bedrock-cdk/tsconfig.json | 32 +++ .../utils/apigwRequest.ts | 45 ++++ .../utils/appsyncRequest.ts | 68 +++++ appsync-events-bedrock-cdk/utils/outputs.ts | 24 ++ 36 files changed, 1372 insertions(+) create mode 100644 appsync-events-bedrock-cdk/.gitignore create mode 100644 appsync-events-bedrock-cdk/.npmignore create mode 100644 appsync-events-bedrock-cdk/README.md create mode 100644 appsync-events-bedrock-cdk/bin/appsync-events-bedrock-cdk.ts create mode 100644 appsync-events-bedrock-cdk/cdk-outputs.json create mode 100644 appsync-events-bedrock-cdk/cdk.json create mode 100644 appsync-events-bedrock-cdk/demo-app/.gitignore create mode 100644 appsync-events-bedrock-cdk/demo-app/README.md create mode 100644 appsync-events-bedrock-cdk/demo-app/index.html create mode 100644 appsync-events-bedrock-cdk/demo-app/package.json create mode 100644 appsync-events-bedrock-cdk/demo-app/public/vite.svg create mode 100644 appsync-events-bedrock-cdk/demo-app/src/App.css create mode 100644 appsync-events-bedrock-cdk/demo-app/src/App.tsx create mode 100644 appsync-events-bedrock-cdk/demo-app/src/ChatComponent.tsx create mode 100644 appsync-events-bedrock-cdk/demo-app/src/assets/react.svg create mode 100644 appsync-events-bedrock-cdk/demo-app/src/index.css create mode 100644 appsync-events-bedrock-cdk/demo-app/src/main.tsx create mode 100644 appsync-events-bedrock-cdk/demo-app/tsconfig.app.json create mode 100644 appsync-events-bedrock-cdk/demo-app/tsconfig.json create mode 100644 appsync-events-bedrock-cdk/demo-app/tsconfig.node.json create mode 100644 appsync-events-bedrock-cdk/demo-app/vite.config.ts create mode 100644 appsync-events-bedrock-cdk/diagram.png create mode 100644 appsync-events-bedrock-cdk/example-pattern.json create mode 100644 appsync-events-bedrock-cdk/jest.config.cjs create mode 100644 appsync-events-bedrock-cdk/lib/appsync-events-bedrock-cdk-stack.ts create mode 100644 appsync-events-bedrock-cdk/lib/events-api-cfn.yaml create mode 100644 appsync-events-bedrock-cdk/package.json create mode 100644 appsync-events-bedrock-cdk/src/chat.ts create mode 100644 appsync-events-bedrock-cdk/step1.png create mode 100644 appsync-events-bedrock-cdk/step2.png create mode 100644 appsync-events-bedrock-cdk/step3.png create mode 100644 appsync-events-bedrock-cdk/test/appsync-events-bedrock-cdk.test.ts create mode 100644 appsync-events-bedrock-cdk/tsconfig.json create mode 100644 appsync-events-bedrock-cdk/utils/apigwRequest.ts create mode 100644 appsync-events-bedrock-cdk/utils/appsyncRequest.ts create mode 100644 appsync-events-bedrock-cdk/utils/outputs.ts diff --git a/appsync-events-bedrock-cdk/.gitignore b/appsync-events-bedrock-cdk/.gitignore new file mode 100644 index 000000000..f60797b6a --- /dev/null +++ b/appsync-events-bedrock-cdk/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/appsync-events-bedrock-cdk/.npmignore b/appsync-events-bedrock-cdk/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/appsync-events-bedrock-cdk/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/appsync-events-bedrock-cdk/README.md b/appsync-events-bedrock-cdk/README.md new file mode 100644 index 000000000..df982013f --- /dev/null +++ b/appsync-events-bedrock-cdk/README.md @@ -0,0 +1,94 @@ +# Stream Amazon Bedrock completions via AWS AppSync Events API + +This pattern shows how to stream Amazon Bedrock completions via AWS AppSync Events API. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/appsync-events-bedrock + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + + +## Requirements +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [Node and NPM](https://nodejs.org/en/download/) installed +* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/v2/guide/cli.html) (AWS CDK) installed +* Make sure to enable the **Anthropic - Claude 3 Haiku** model on the [Bedrock console](https://console.aws.amazon.com/bedrock/home#/modelaccess). + + +## How it works +![diagram](diagram.png) +1. The Client connects to the Websocket using the Amplify client or a custom made library following the [instructions described in the documentation](https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-websocket-protocol.html); +2. The Client chooses a namespace or a channel (or a segment) and subscribes to it. [Additional info the documentation](https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-websocket-protocol.html); +3. The Client performs a request towards the API Gateway that exposes a `/chat` route passing the `userId` and `prompt` parameters. The `userId` will be used as a segment for the channel. The prompt will be used for GenAI completions on Bedrock; +4. The triggered Lambda receives payload; +5. The Lambda proceeds to invoke a Bedrock model using the streaming API; +6. The Lambda will receive the completion `chunks` as soon as they are available; +7. For each completion chunk, the Lambda will publish them through the [Publish HTTP API on AppSync Events](https://docs.aws.amazon.com/appsync/latest/eventapi/publish-http.html); +8. The Client is now able to retrieve the completion chunks from AppSync Events; +9. The Client can finally unsubscribe from the channel and close the Websocket connection. + +### Permissions and Security +In this demo the Client is only able to subscribe to the AppSync Event Websocket via the API_KEY. The same API_KEY will be used by the Lambda to perform Publish requests. + + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: +```sh +git clone https://github.com/aws-samples/serverless-patterns +``` +2. Change directory to the pattern directory: +```sh +cd appsync-events-bedrock-cdk +``` +3. Install the required dependencies: +```sh +npm install +``` +4. Deploy the stack to your default AWS account and region: +```sh +npm run deploy +``` +5. Export the environment variables for the demo web app: +```sh +npm run export-outputs +``` +6. Install dependencies for the demo web app: +```sh +cd demo-app && npm install +``` +7. Launch the demo web app +```sh +npm run dev +``` + +## Testing +Integration tests can be ran with jest from the pattern's root directory: +```sh +npm run test +``` + +From the demo app it's possible to see all connection events and data events in a structured way. +First of all connect to the Websocket +![step1](step1.png) + +Then subscribe to the namespace. Please note: The default namespace is `BedrockChat`. By inserting `BedrockChat/*` you will be able to receive all the messages in the sub-segments. +![step2](step2.png) + +Finally, insert your username (the final segment will become `BedrockChat/username`) and your prompt. +![step3](step3.png) + +By clicking on the `realtime` entry on the networking tab and selecting `messages` you will see all the messages exchanged between the client demo app and AppSync Event API. For convienence I've setup two scrollable boxes that display the same data. + + +## Cleanup +Close the demo web app by hitting `^C` on MacOs. + +Return to the pattern's root folder and delete the stack: +```sh +cdk destroy --all +``` +---- +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 diff --git a/appsync-events-bedrock-cdk/bin/appsync-events-bedrock-cdk.ts b/appsync-events-bedrock-cdk/bin/appsync-events-bedrock-cdk.ts new file mode 100644 index 000000000..bdd9465a1 --- /dev/null +++ b/appsync-events-bedrock-cdk/bin/appsync-events-bedrock-cdk.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { AppsyncEventsBedrockCdkStack } from '../lib/appsync-events-bedrock-cdk-stack'; + +const app = new cdk.App(); +new AppsyncEventsBedrockCdkStack(app, 'AppsyncEventsBedrockCdkStack', { + /* If you don't specify 'env', this stack will be environment-agnostic. + * Account/Region-dependent features and context lookups will not work, + * but a single synthesized template can be deployed anywhere. */ + + /* Uncomment the next line to specialize this stack for the AWS Account + * and Region that are implied by the current CLI configuration. */ + // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, + + /* Uncomment the next line if you know exactly what Account and Region you + * want to deploy the stack to. */ + // env: { account: '123456789012', region: 'us-east-1' }, + + /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ +}); \ No newline at end of file diff --git a/appsync-events-bedrock-cdk/cdk-outputs.json b/appsync-events-bedrock-cdk/cdk-outputs.json new file mode 100644 index 000000000..a69e7a8db --- /dev/null +++ b/appsync-events-bedrock-cdk/cdk-outputs.json @@ -0,0 +1,10 @@ +{ + "AppsyncEventsBedrockCdkStack": { + "ApiKey": "da2-imdsnscd6jfa3fpyjv57db6zyu", + "ChannelName": "BedrockChat", + "Region": "eu-central-1", + "EventsApiHttp": "6fsvhbewzjbuziulxwuastj2hi.appsync-api.eu-central-1.amazonaws.com", + "EventsApiRealtime": "6fsvhbewzjbuziulxwuastj2hi.appsync-realtime-api.eu-central-1.amazonaws.com", + "ChatApiUrl": "https://tcv8ilbelj.execute-api.eu-central-1.amazonaws.com" + } +} diff --git a/appsync-events-bedrock-cdk/cdk.json b/appsync-events-bedrock-cdk/cdk.json new file mode 100644 index 000000000..705e36c4f --- /dev/null +++ b/appsync-events-bedrock-cdk/cdk.json @@ -0,0 +1,80 @@ +{ + "app": "npx tsx bin/appsync-events-bedrock-cdk.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true + } +} diff --git a/appsync-events-bedrock-cdk/demo-app/.gitignore b/appsync-events-bedrock-cdk/demo-app/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/appsync-events-bedrock-cdk/demo-app/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/appsync-events-bedrock-cdk/demo-app/README.md b/appsync-events-bedrock-cdk/demo-app/README.md new file mode 100644 index 000000000..ba94e3c72 --- /dev/null +++ b/appsync-events-bedrock-cdk/demo-app/README.md @@ -0,0 +1 @@ +Follow instructions in the pattern's root folder [README](../README.md). \ No newline at end of file diff --git a/appsync-events-bedrock-cdk/demo-app/index.html b/appsync-events-bedrock-cdk/demo-app/index.html new file mode 100644 index 000000000..e4b78eae1 --- /dev/null +++ b/appsync-events-bedrock-cdk/demo-app/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/appsync-events-bedrock-cdk/demo-app/package.json b/appsync-events-bedrock-cdk/demo-app/package.json new file mode 100644 index 000000000..a54871f36 --- /dev/null +++ b/appsync-events-bedrock-cdk/demo-app/package.json @@ -0,0 +1,34 @@ +{ + "name": "demo-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@types/uuid": "^10.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "uuid": "^11.0.2" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react-swc": "^3.5.0", + "esbuild": "^0.24.0", + "eslint": "^9.13.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.11.0", + "ts-node": "^10.9.2", + "typescript": "~5.6.2", + "typescript-eslint": "^8.11.0", + "vite": "^5.4.10", + "ws": "^8.18.0" + } +} diff --git a/appsync-events-bedrock-cdk/demo-app/public/vite.svg b/appsync-events-bedrock-cdk/demo-app/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/appsync-events-bedrock-cdk/demo-app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/appsync-events-bedrock-cdk/demo-app/src/App.css b/appsync-events-bedrock-cdk/demo-app/src/App.css new file mode 100644 index 000000000..b9d355df2 --- /dev/null +++ b/appsync-events-bedrock-cdk/demo-app/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/appsync-events-bedrock-cdk/demo-app/src/App.tsx b/appsync-events-bedrock-cdk/demo-app/src/App.tsx new file mode 100644 index 000000000..e6634c034 --- /dev/null +++ b/appsync-events-bedrock-cdk/demo-app/src/App.tsx @@ -0,0 +1,13 @@ +import './App.css' +import ChatComponent from './ChatComponent' + +function App() { + + return ( + <> + + + ) +} + +export default App diff --git a/appsync-events-bedrock-cdk/demo-app/src/ChatComponent.tsx b/appsync-events-bedrock-cdk/demo-app/src/ChatComponent.tsx new file mode 100644 index 000000000..b52df37ab --- /dev/null +++ b/appsync-events-bedrock-cdk/demo-app/src/ChatComponent.tsx @@ -0,0 +1,253 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { makeRequest } from "../../utils/apigwRequest"; + +const ChatComponent: React.FC = () => { + const [wsConnection, setWsConnection] = useState(null); + const [channelName, setChannelName] = useState(''); + const [userId, setUserId] = useState(''); + const [prompt, setPrompt] = useState(''); + const [messages, setMessages] = useState([]); + const [connectionEvents, setConnectionEvents] = useState([]); + const [isSubscribed, setIsSubscribed] = useState(false); + const [subscriptionId, setSubscriptionId] = useState(''); + + const messagesEndRef = useRef(null); + const connectionEventsEndRef = useRef(null); + + const wssUrl = `wss://${import.meta.env.VITE_EVENTS_API_REAL_TIME}/event/realtime`; + const authorization = { + host: import.meta.env.VITE_EVENTS_API_HTTP, + "x-api-key": import.meta.env.VITE_API_KEY + }; + + useEffect(() => { + if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [messages]); + + useEffect(() => { + if (connectionEventsEndRef.current) { + connectionEventsEndRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [connectionEvents]); + + function getBase64URLEncoded(authorization: any): string { + return btoa(JSON.stringify(authorization)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + } + + function getSubprotocols(authorization: any): string[] { + const header = getBase64URLEncoded(authorization); + return ['aws-appsync-event-ws', `header-${header}`]; + } + + const initializeWebSocket = () => { + const ws = new WebSocket(wssUrl, getSubprotocols(authorization)); + + ws.onopen = () => { + console.log('WebSocket Connected'); + ws.send(JSON.stringify({ type: 'connection_init' })); + } + + ws.onclose = (event) => { + console.log('WebSocket connection closed:', event.code, event.reason); + setConnectionEvents(prev => [...prev, `WebSocket connection closed: ${event.code}, ${event.reason}`]); + } + + ws.onerror = (error) => { + console.log('WebSocket Error:', error); + setConnectionEvents(prev => [...prev, `WebSocket Error: ${error}`]); + } + + ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + if (message.type === 'connection_ack') { + setConnectionEvents(prev => [...prev, 'connection_ack']); + } else if (message.type === 'ka') { + setConnectionEvents(prev => [...prev, 'ka']); + } else if (message.type === 'subscribe_success' && message.id === subscriptionId) { + setIsSubscribed(true); + setConnectionEvents(prev => [...prev, 'subscribe_success']); + } else if (message.type === 'unsubscribe_success' && message.id === subscriptionId) { + setIsSubscribed(false); + setConnectionEvents(prev => [...prev, 'unsubscribe_success']); + } else if (message.type === 'unsubscribe_error' && message.id === subscriptionId) { + setIsSubscribed(false); + setConnectionEvents(prev => [...prev, 'unsubscribe_error']); + } else if (message.type === 'data') { + // Handle data events here + console.log(message) + setMessages(prev => [...prev, JSON.parse(message.event).data]); + } else { + console.log('Unhandled message type:', message.type, message); + } + } catch (error) { + console.error('Error parsing WebSocket message:', error); + } + }; + + setWsConnection(ws); + }; + + + const subscribeToChannel = async () => { + if (wsConnection && channelName) { + const newSubscriptionId = uuidv4(); + setSubscriptionId(newSubscriptionId); + + wsConnection.send(JSON.stringify({ + type: 'subscribe', + id: newSubscriptionId, + channel: channelName, + authorization + })); + + try { + await new Promise((resolve, reject) => { + const subscriptionTimeoutMs = 10000; // 10 seconds + const subscriptionTimer = setTimeout(() => { + reject(new Error('Subscription timeout')); + }, subscriptionTimeoutMs); + + const messageHandler = (event: MessageEvent) => { + const message = JSON.parse(event.data.toString()); + if (message.type === 'subscribe_success' && message.id === newSubscriptionId) { + clearTimeout(subscriptionTimer); + wsConnection.removeEventListener('message', messageHandler); + resolve(); + } + }; + + wsConnection.addEventListener('message', messageHandler); + }); + + setIsSubscribed(true); + setConnectionEvents(prev => [...prev, `subscribe - ${channelName}`]); + } catch (error: any) { + console.error('Subscription failed:', error); + setMessages(prev => [...prev, `Subscription failed: ${error.message}`]); + } + } + }; + + const unsubscribe = () => { + if (wsConnection && isSubscribed) { + wsConnection.send(JSON.stringify({ + type: 'unsubscribe', + id: subscriptionId + })); + setConnectionEvents(prev => [...prev, `unsubscribe - ${channelName}`]); + setIsSubscribed(false) + } + }; + + const closeWebSocket = () => { + if (wsConnection) { + wsConnection.close(); + setWsConnection(null); + setIsSubscribed(false); + setSubscriptionId(''); + setConnectionEvents(prev => [...prev, 'WebSocket connection closed']); + } + }; + + const sendMessage = async () => { + if (wsConnection && userId && prompt) { + const res = makeRequest({ + url: import.meta.env.VITE_CHAT_API_URL || "", + method: "POST", + apiKey: import.meta.env.VITE_API_KEY + }, { + prompt: prompt, + userId: userId + }); + + //console.log(res) + + setPrompt(''); + } + }; + + const clearMessages = () => { + setMessages([]); + }; + + const clearConnectionEvents = () => { + setConnectionEvents([]); + }; + + return ( +
+
+ +
+
+ setChannelName(e.target.value)} + placeholder={`${import.meta.env.VITE_CHANNEL}/*`} + disabled={isSubscribed} + /> + +
+
+ +
+
+ +
+
+ setUserId(e.target.value)} + placeholder="User ID" + /> +
+
+