diff --git a/packages/aws-amplify-react/package.json b/packages/aws-amplify-react/package.json index b7fac5521ed..603c1a22f02 100644 --- a/packages/aws-amplify-react/package.json +++ b/packages/aws-amplify-react/package.json @@ -108,6 +108,7 @@ } }, "dependencies": { + "pako": "^2.0.4", "qrcode.react": "^0.8.0", "regenerator-runtime": "^0.11.1" }, diff --git a/packages/aws-amplify-react/src/Amplify-UI/data-test-attributes.tsx b/packages/aws-amplify-react/src/Amplify-UI/data-test-attributes.tsx index 0fa40c3a59b..c23379b206a 100644 --- a/packages/aws-amplify-react/src/Amplify-UI/data-test-attributes.tsx +++ b/packages/aws-amplify-react/src/Amplify-UI/data-test-attributes.tsx @@ -150,6 +150,13 @@ export const chatBot = { sendMessageButton: 'chatbot-send-message-button', }; +export const chatBotLex2 = { + title: 'chatbot-lex-2-title', + dialog: 'chatbot-lex-2-dialog', + messageInput: 'chatbot-lex-2-message-input', + sendMessageButton: 'chatbot-lex-2-send-message-button', +}; + export const auth = { signIn, signOut, diff --git a/packages/aws-amplify-react/src/Interactions/Lex2ChatBot.tsx b/packages/aws-amplify-react/src/Interactions/Lex2ChatBot.tsx new file mode 100644 index 00000000000..5a60ac6d7d9 --- /dev/null +++ b/packages/aws-amplify-react/src/Interactions/Lex2ChatBot.tsx @@ -0,0 +1,696 @@ +/* + * Copyright 2017-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as React from 'react'; +import { + FormSection, + SectionHeader, + SectionBody, + SectionFooter, +} from '../AmplifyUI'; +import { Input, Button } from '../AmplifyTheme'; + +import { I18n } from '@aws-amplify/core'; +import { Interactions } from '@aws-amplify/interactions'; +import { ConsoleLogger as Logger } from '@aws-amplify/core'; +import { chatBotLex2 } from '../Amplify-UI/data-test-attributes'; +const logger = new Logger('ChatBot'); + +// @ts-ignore +const styles: { [pname: string]: React.CSSProperties } = { + itemMe: { + padding: 10, + fontSize: 12, + color: 'gray', + marginTop: 4, + textAlign: 'right', + margin: 10, + }, + itemBot: { + fontSize: 12, + textAlign: 'left', + margin: 10, + }, + list: { + height: '300px', + overflow: 'auto', + }, + // @ts-ignore + textInput: Object.assign({}, Input, { + display: 'inline-block', + width: 'calc(100% - 90px - 15px)', + }), + // @ts-ignore + button: Object.assign({}, Button, { + width: '60px', + float: 'right', + }), + // @ts-ignore + mic: Object.assign({}, Button, { + width: '40px', + float: 'right', + }), +}; + +const STATES = { + INITIAL: { MESSAGE: 'Type your message or click 🎤', ICON: '🎤' }, + LISTENING: { MESSAGE: 'Listening... click 🔴 again to cancel', ICON: '🔴' }, + SENDING: { MESSAGE: 'Please wait...', ICON: '🔊' }, + SPEAKING: { MESSAGE: 'Speaking...', ICON: '...' }, +}; + +const defaultVoiceConfig = { + silenceDetectionConfig: { + time: 2000, + amplitude: 0.2, + }, +}; + +// @ts-ignore +let audioControl; + +export interface IChatBotProps { + botName?: string; + clearOnComplete?: boolean; + conversationModeOn?: boolean; + onComplete: any; + textEnabled?: boolean; + theme?: any; + title?: string; + voiceConfig?: any; + voiceEnabled?: boolean; + welcomeMessage?: string; + initialMessage?: string; +} + +export interface IChatBotDialog { + message: object | string; + from: string; +} + +export interface IChatBotState { + audioInput?: any; + continueConversation: boolean; + currentVoiceState: any; + dialog: IChatBotDialog[]; + inputDisabled: boolean; + inputText: string; + lexResponse?: any; + micButtonDisabled: boolean; + micText: string; +} +interface Lex2MessageRenderProps { + message: any; + enabled: boolean; + sendMessage: (message: string) => void; +} + +const Lex2MessageRender = ({ + message, +}: Lex2MessageRenderProps): React.ReactElement => { + switch (message.contentType) { + case 'CustomPayload': + return
CustomPayload
; + case 'ImageResponseCard': + return
ImageResponseCard
; + case 'PlainText': + return
PlainText: {message.content}
; + case 'SSML': + return
SSML
; + } + return
UnknownMessageContentType
; +}; + +export class Lex2ChatBot extends React.Component { + private listItemsRef: any; + + // @ts-ignore + constructor(props) { + super(props); + if (this.props.voiceEnabled) { + require('./aws-lex-audio'); + // @ts-ignore + audioControl = new global.LexAudio.audioControl(); + } + if (!this.props.textEnabled && this.props.voiceEnabled) { + STATES.INITIAL.MESSAGE = 'Click the mic button'; + // @ts-ignore + styles.textInput = Object.assign({}, Input, { + display: 'inline-block', + width: 'calc(100% - 40px - 15px)', + }); + } + if (this.props.textEnabled && !this.props.voiceEnabled) { + STATES.INITIAL.MESSAGE = 'Type a message'; + // @ts-ignore + styles.textInput = Object.assign({}, Input, { + display: 'inline-block', + width: 'calc(100% - 60px - 15px)', + }); + } + if (!this.props.voiceConfig.silenceDetectionConfig) { + throw new Error('voiceConfig prop is missing silenceDetectionConfig'); + } + + this.state = { + dialog: this.props.welcomeMessage + ? [ + { + message: this.props.welcomeMessage, + from: 'system', + }, + ] + : [], + inputText: '', + currentVoiceState: STATES.INITIAL, + inputDisabled: false, + micText: STATES.INITIAL.ICON, + continueConversation: false, + micButtonDisabled: false, + }; + this.micButtonHandler = this.micButtonHandler.bind(this); + this.changeInputText = this.changeInputText.bind(this); + this.listItems = this.listItems.bind(this); + this.submit = this.submit.bind(this); + // @ts-ignore + this.listItemsRef = React.createRef(); + this.onSilenceHandler = this.onSilenceHandler.bind(this); + this.doneSpeakingHandler = this.doneSpeakingHandler.bind(this); + this.lexResponseHandler = this.lexResponseHandler.bind(this); + this.sendMessage = this.sendMessage.bind(this); + } + + async micButtonHandler() { + if (this.state.continueConversation) { + this.reset(); + } else { + this.setState( + { + inputDisabled: true, + continueConversation: true, + currentVoiceState: STATES.LISTENING, + micText: STATES.LISTENING.ICON, + micButtonDisabled: false, + }, + () => { + // @ts-ignore + audioControl.startRecording( + this.onSilenceHandler, + null, + this.props.voiceConfig.silenceDetectionConfig + ); + } + ); + } + } + + onSilenceHandler() { + // @ts-ignore + audioControl.stopRecording(); + if (!this.state.continueConversation) { + return; + } + // @ts-ignore + audioControl.exportWAV(blob => { + this.setState( + { + currentVoiceState: STATES.SENDING, + audioInput: blob, + micText: STATES.SENDING.ICON, + micButtonDisabled: true, + }, + () => { + this.lexResponseHandler(); + } + ); + }); + } + + async lexResponseHandler() { + if (!Interactions || typeof Interactions.send !== 'function') { + throw new Error( + 'No Interactions module found, please ensure @aws-amplify/interactions is imported' + ); + } + if (!this.state.continueConversation) { + return; + } + + const interactionsMessage = { + content: this.state.audioInput, + options: { + messageType: 'voice', + }, + }; + const response = await Interactions.send( + // @ts-ignore + this.props.botName, + interactionsMessage + ); + const decodedMessages = response.messages + ? unGzipBase64AsJson(response.messages) + : []; + const decodedTranscript = response.inputTranscript + ? unGzipBase64AsJson(response.inputTranscript) + : 'Voice Message'; + const messages = decodedMessages?.map(i => ({ + from: 'bot', + message: i, + })); + this.setState( + { + lexResponse: response, + currentVoiceState: STATES.SPEAKING, + micText: STATES.SPEAKING.ICON, + micButtonDisabled: true, + dialog: [ + ...this.state.dialog, + { message: decodedTranscript, from: 'me' }, + ...messages, + ], + inputText: '', + }, + () => { + this.doneSpeakingHandler(); + } + ); + this.listItemsRef.current.scrollTop = + this.listItemsRef.current.scrollHeight; + } + + doneSpeakingHandler() { + if (!this.state.continueConversation) { + return; + } + if (this.state.lexResponse.contentType === 'audio/mpeg') { + // @ts-ignore + audioControl.play(this.state.lexResponse.audioStream, () => { + if ( + this.state.lexResponse.dialogState === 'ReadyForFulfillment' || + this.state.lexResponse.dialogState === 'Fulfilled' || + this.state.lexResponse.dialogState === 'Failed' || + !this.props.conversationModeOn + ) { + this.setState({ + inputDisabled: false, + currentVoiceState: STATES.INITIAL, + micText: STATES.INITIAL.ICON, + micButtonDisabled: false, + continueConversation: false, + }); + } else { + this.setState( + { + currentVoiceState: STATES.LISTENING, + micText: STATES.LISTENING.ICON, + micButtonDisabled: false, + }, + () => { + // @ts-ignore + audioControl.startRecording( + this.onSilenceHandler, + null, + this.props.voiceConfig.silenceDetectionConfig + ); + } + ); + } + }); + } else { + this.setState({ + inputDisabled: false, + currentVoiceState: STATES.INITIAL, + micText: STATES.INITIAL.ICON, + micButtonDisabled: false, + continueConversation: false, + }); + } + } + + reset() { + this.setState( + { + inputText: '', + currentVoiceState: STATES.INITIAL, + inputDisabled: false, + micText: STATES.INITIAL.ICON, + continueConversation: false, + micButtonDisabled: false, + }, + () => { + // @ts-ignore + audioControl.clear(); + } + ); + } + + listItems() { + const lastIndex = this.state.dialog.length - 1; + return this.state.dialog.map((m, i) => { + if (m.from === 'me') { + return ( +
+ {typeof m.message === 'string' ? ( +
{m.message}
+ ) : ( + + )} +
+ ); + } else if (m.from === 'system') { + return ( +
+ {typeof m.message === 'string' ? ( +
{m.message}
+ ) : ( + + )} +
+ ); + } else { + return ( +
+ {typeof m.message === 'string' ? ( +
{m.message}
+ ) : ( + + )} +
+ ); + } + }); + } + + async sendMessage(text: string) { + const response = await Interactions.send( + // @ts-ignore + this.props.botName, + text + ); + const messages = response?.messages?.map(i => ({ + from: 'bot', + message: i, + })); + this.setState({ + // @ts-ignore + dialog: [ + ...this.state.dialog, + // @ts-ignore + ...messages, + ], + inputText: '', + }); + this.listItemsRef.current.scrollTop = + this.listItemsRef.current.scrollHeight; + } + + // @ts-ignore + async submit(e) { + e?.preventDefault(); + + if (!this.state.inputText) { + return; + } + + await new Promise(resolve => + this.setState( + { + dialog: [ + ...this.state.dialog, + { message: this.state.inputText, from: 'me' }, + ], + }, + // @ts-ignore + resolve + ) + ); + + if (!Interactions || typeof Interactions.send !== 'function') { + throw new Error( + 'No Interactions module found, please ensure @aws-amplify/interactions is imported' + ); + } + + this.sendMessage(this.state.inputText); + } + + // @ts-ignore + async changeInputText(event) { + await this.setState({ inputText: event.target.value }); + } + // @ts-ignore + getOnComplete(fn) { + // @ts-ignore + return (...args) => { + const { clearOnComplete } = this.props; + const message = fn(...args); + + this.setState( + { + dialog: [ + ...(clearOnComplete ? [] : this.state.dialog), + message && { from: 'bot', message }, + ].filter(Boolean), + }, + () => { + this.listItemsRef.current.scrollTop = + this.listItemsRef.current.scrollHeight; + } + ); + }; + } + + componentDidMount() { + const { onComplete, botName, initialMessage } = this.props; + + if (onComplete && botName) { + if (!Interactions || typeof Interactions.onComplete !== 'function') { + throw new Error( + 'No Interactions module found, please ensure @aws-amplify/interactions is imported' + ); + } + // @ts-ignore + Interactions.onComplete(botName, this.getOnComplete(onComplete, this)); + } + if (initialMessage) this.sendMessage(initialMessage); + } + + // @ts-ignore + componentDidUpdate(prevProps) { + const { onComplete, botName } = this.props; + + if (botName && this.props.onComplete !== prevProps.onComplete) { + if (!Interactions || typeof Interactions.onComplete !== 'function') { + throw new Error( + 'No Interactions module found, please ensure @aws-amplify/interactions is imported' + ); + } + // @ts-ignore + Interactions.onComplete(botName, this.getOnComplete(onComplete, this)); + } + } + + render() { + const { title, theme } = this.props; + + return ( + + {title && ( + +
{I18n.get(title)}
+
+ )} + +
+ {this.listItems()} +
+
+ + + +
+ ); + } +} + +// @ts-ignore +function ChatBotTextInput(props) { + const styles = props.styles; + const onChange = props.onChange; + const inputText = props.inputText; + const inputDisabled = props.inputDisabled; + const currentVoiceState = props.currentVoiceState; + + return ( + + ); +} + +// @ts-ignore +function ChatBotMicButton(props) { + const voiceEnabled = props.voiceEnabled; + const styles = props.styles; + const micButtonDisabled = props.micButtonDisabled; + const handleMicButton = props.handleMicButton; + const micText = props.micText; + + if (!voiceEnabled) { + return null; + } + + return ( + + ); +} + +// @ts-ignore +function ChatBotTextButton(props) { + const textEnabled = props.textEnabled; + const styles = props.styles; + const inputDisabled = props.inputDisabled; + + if (!textEnabled) { + return null; + } + + return ( + + ); +} + +// @ts-ignore +function ChatBotInputs(props) { + const voiceEnabled = props.voiceEnabled; + const textEnabled = props.textEnabled; + const styles = props.styles; + const onChange = props.onChange; + let inputDisabled = props.inputDisabled; + const micButtonDisabled = props.micButtonDisabled; + const inputText = props.inputText; + const onSubmit = props.onSubmit; + const handleMicButton = props.handleMicButton; + const micText = props.micText; + const currentVoiceState = props.currentVoiceState; + + if (voiceEnabled && !textEnabled) { + inputDisabled = true; + } + + if (!voiceEnabled && !textEnabled) { + return ( +
+ No Chatbot inputs enabled. Set at least one of voiceEnabled or + textEnabled in the props.{' '} +
+ ); + } + + return ( +
+ + + + + ); +} + +// @ts-ignore +Lex2ChatBot.defaultProps = { + title: '', + botName: '', + onComplete: undefined, + clearOnComplete: false, + voiceConfig: defaultVoiceConfig, + conversationModeOn: false, + voiceEnabled: false, + textEnabled: true, +}; + +export default Lex2ChatBot; diff --git a/packages/aws-amplify-react/src/Interactions/helpers/convert.ts b/packages/aws-amplify-react/src/Interactions/helpers/convert.ts new file mode 100644 index 00000000000..591a5f3701e --- /dev/null +++ b/packages/aws-amplify-react/src/Interactions/helpers/convert.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import pako from 'pako'; + +export const base64ToArrayBuffer = (base64: string): Uint8Array => { + const binary_string = window.atob(base64); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes; +}; + +export const unGzipBase64AsJson = (gzipBase64: string): T => { + return JSON.parse( + pako.ungzip(base64ToArrayBuffer(gzipBase64), { to: 'string' }) + ) as T; +}; diff --git a/packages/interactions/package.json b/packages/interactions/package.json index d881be828ef..90b060289e0 100644 --- a/packages/interactions/package.json +++ b/packages/interactions/package.json @@ -42,7 +42,9 @@ "homepage": "https://aws-amplify.github.io/", "dependencies": { "@aws-amplify/core": "4.6.1", - "@aws-sdk/client-lex-runtime-service": "3.6.1" + "@aws-sdk/client-lex-runtime-service": "3.6.1", + "@aws-sdk/client-lex-runtime-v2": "^3.31.0", + "pako": "^2.0.4" }, "jest": { "globals": { diff --git a/packages/interactions/src/Providers/AWSLex2Provider.ts b/packages/interactions/src/Providers/AWSLex2Provider.ts new file mode 100644 index 00000000000..ea432a7faaa --- /dev/null +++ b/packages/interactions/src/Providers/AWSLex2Provider.ts @@ -0,0 +1,197 @@ +/* + * Copyright 2017-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { AbstractInteractionsProvider } from './InteractionsProvider'; +import { + InteractionsOptions, + InteractionsResponse, + InteractionsMessage, +} from '../types'; +import { + LexRuntimeV2Client, + RecognizeTextCommand, + RecognizeTextCommandInput, + RecognizeUtteranceCommand, + RecognizeUtteranceCommandInput, + SessionState, +} from '@aws-sdk/client-lex-runtime-v2'; +import { + ConsoleLogger as Logger, + Credentials, + getAmplifyUserAgent, +} from '@aws-amplify/core'; +import { convert, unGzipBase64AsJson } from './AWSLexProviderHelper/convert'; + +const logger = new Logger('AWSLex2Provider'); + +export class AWSLex2Provider extends AbstractInteractionsProvider { + // @ts-ignore + private lex2RuntimeServiceClient: LexRuntimeV2Client; + private _botsCompleteCallback: object; + + constructor(options: InteractionsOptions = {}) { + super(options); + this._botsCompleteCallback = {}; + } + + getProviderName() { + return 'AWSLex2Provider'; + } + + reportBotStatus(botname: string, sessionState?: SessionState) { + // Check if state is fulfilled to resolve onFullfilment promise + logger.debug('postContent state', sessionState?.intent?.state); + if ( + sessionState?.intent?.state === 'ReadyForFulfillment' || + sessionState?.intent?.state === 'Fulfilled' + ) { + // @ts-ignore + if (typeof this._botsCompleteCallback[botname] === 'function') { + setTimeout( + () => + // @ts-ignore + this._botsCompleteCallback[botname](null, { + slots: sessionState?.intent?.slots, + }), + 0 + ); + } + + if ( + this._config && + typeof this._config[botname].onComplete === 'function' + ) { + setTimeout( + () => + this._config[botname].onComplete(null, { + slots: sessionState?.intent?.slots, + }), + 0 + ); + } + } + + if (sessionState?.intent?.state === 'Failed') { + // @ts-ignore + if (typeof this._botsCompleteCallback[botname] === 'function') { + setTimeout( + // @ts-ignore + () => this._botsCompleteCallback[botname]('Bot conversation failed'), + 0 + ); + } + + if ( + this._config && + typeof this._config[botname].onComplete === 'function' + ) { + setTimeout( + () => this._config[botname].onComplete('Bot conversation failed'), + 0 + ); + } + } + } + + async sendMessage( + botname: string, + message: string | InteractionsMessage + ): Promise { + if (!this._config[botname]) { + return Promise.reject('Bot ' + botname + ' does not exist'); + } + const credentials = await Credentials.get(); + if (!credentials) { + return Promise.reject('No credentials'); + } + this.lex2RuntimeServiceClient = new LexRuntimeV2Client({ + region: this._config[botname].region, + credentials, + customUserAgent: getAmplifyUserAgent(), + }); + let params: RecognizeTextCommandInput | RecognizeUtteranceCommandInput; + if (typeof message === 'string') { + params = { + botAliasId: this._config[botname].botAliasId, + botId: this._config[botname].botId, + localeId: this._config[botname].localeId, + text: message, + sessionId: credentials.identityId, + }; + + logger.debug('postText to lex2', message); + + try { + const postTextCommand = new RecognizeTextCommand(params); + const data = await this.lex2RuntimeServiceClient.send(postTextCommand); + this.reportBotStatus(botname, data.sessionState); + return data; + } catch (err) { + return Promise.reject(err); + } + } else { + const { + content, + options: { messageType }, + } = message; + + if (messageType === 'voice') { + params = { + botAliasId: this._config[botname].botAliasId, + botId: this._config[botname].botId, + localeId: this._config[botname].localeId, + sessionId: credentials.identityId, + requestContentType: 'audio/x-l16; sample-rate=16000; channel-count=1', + inputStream: await convert(content as Blob), + } as RecognizeUtteranceCommandInput; + } else { + params = { + botAliasId: this._config[botname].botAliasId, + botId: this._config[botname].botId, + localeId: this._config[botname].localeId, + sessionId: credentials.identityId, + requestContentType: 'text/plain; charset=utf-8', + inputStream: content as string, + } as RecognizeUtteranceCommandInput; + } + logger.debug('postContent to lex2', message); + try { + // @ts-ignore + const postContentCommand = new RecognizeUtteranceCommand(params); + const data = await this.lex2RuntimeServiceClient.send( + postContentCommand + ); + // @ts-ignore + const audioArray = await convert(data.audioStream); + this.reportBotStatus( + botname, + data.sessionState + ? unGzipBase64AsJson(data.sessionState) + : undefined + ); + return { ...data, ...{ audioStream: audioArray } }; + } catch (err) { + return Promise.reject(err); + } + } + } + + // @ts-ignore + onComplete(botname: string, callback) { + if (!this._config[botname]) { + throw new ErrorEvent('Bot ' + botname + ' does not exist'); + } + // @ts-ignore + this._botsCompleteCallback[botname] = callback; + } +} diff --git a/packages/interactions/src/Providers/AWSLexProviderHelper/convert.native.ts b/packages/interactions/src/Providers/AWSLexProviderHelper/convert.native.ts index 15521d87ad8..a32b64710b0 100644 --- a/packages/interactions/src/Providers/AWSLexProviderHelper/convert.native.ts +++ b/packages/interactions/src/Providers/AWSLexProviderHelper/convert.native.ts @@ -1,3 +1,18 @@ +/* + * Copyright 2017-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import pako from 'pako'; + export const convert = (stream: Blob): Promise => { return new Promise(async (res, rej) => { const blobURL = URL.createObjectURL(stream); @@ -11,3 +26,19 @@ export const convert = (stream: Blob): Promise => { request.send(); }); }; + +export const base64ToArrayBuffer = (base64: string): Uint8Array => { + const binary_string = window.atob(base64); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes; +}; + +export const unGzipBase64AsJson = (gzipBase64: string): T => { + return JSON.parse( + pako.ungzip(base64ToArrayBuffer(gzipBase64), { to: 'string' }) + ) as T; +}; diff --git a/packages/interactions/src/Providers/AWSLexProviderHelper/convert.ts b/packages/interactions/src/Providers/AWSLexProviderHelper/convert.ts index 6582f677ca9..7d5c1726a75 100644 --- a/packages/interactions/src/Providers/AWSLexProviderHelper/convert.ts +++ b/packages/interactions/src/Providers/AWSLexProviderHelper/convert.ts @@ -1,4 +1,19 @@ +/* + * Copyright 2017-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + import { Readable } from 'stream'; +import pako from 'pako'; + export const convert = ( stream: Blob | Readable | ReadableStream ): Promise => { @@ -10,3 +25,19 @@ export const convert = ( throw new Error('Readable is not supported.'); } }; + +export const base64ToArrayBuffer = (base64: string): Uint8Array => { + const binary_string = window.atob(base64); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes; +}; + +export const unGzipBase64AsJson = (gzipBase64: string): T => { + return JSON.parse( + pako.ungzip(base64ToArrayBuffer(gzipBase64), { to: 'string' }) + ) as T; +};