UNPKG

aws-amplify-react

Version:

AWS Amplify is a JavaScript library for Frontend and mobile developers building cloud-enabled applications.

458 lines (404 loc) • 14.9 kB
import * as React from 'react'; import { Component } from 'react'; import { Container, FormSection, SectionHeader, SectionBody, SectionFooter } from "../AmplifyUI"; import { Input, Button } from "../AmplifyTheme"; import { I18n } from '@aws-amplify/core'; import Interactions from '@aws-amplify/interactions'; import regeneratorRuntime from 'regenerator-runtime/runtime'; import { ConsoleLogger as Logger } from '@aws-amplify/core'; const logger = new Logger('ChatBot'); const styles = { itemMe: { padding: 10, fontSize: 12, color: 'gray', marginTop: 4, textAlign: 'right' }, itemBot: { fontSize: 12, textAlign: 'left' }, list: { height: '300px', overflow: 'auto', }, textInput: Object.assign({}, Input, { display: 'inline-block', width: 'calc(100% - 90px - 15px)', }), button: Object.assign({}, Button, { width: '60px', float: 'right', }), 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 } } let audioControl; export class ChatBot extends Component { constructor(props) { super(props); if (this.props.voiceEnabled) { require('./aws-lex-audio.js'); audioControl = new global.LexAudio.audioControl(); } if (!this.props.textEnabled && this.props.voiceEnabled) { STATES.INITIAL.MESSAGE = 'Click the mic button'; 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'; 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: [{ message: this.props.welcomeMessage || 'Welcome to Lex', 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); this.listItemsRef = React.createRef(); this.onSilenceHandler = this.onSilenceHandler.bind(this) this.doneSpeakingHandler = this.doneSpeakingHandler.bind(this) this.lexResponseHandler = this.lexResponseHandler.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, }, () => { audioControl.startRecording(this.onSilenceHandler, null, this.props.voiceConfig.silenceDetectionConfig); }) } } onSilenceHandler() { audioControl.stopRecording(); if (!this.state.continueConversation) { return; } 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(this.props.botName, interactionsMessage); this.setState({ lexResponse: response, currentVoiceState: STATES.SPEAKING, micText: STATES.SPEAKING.ICON, micButtonDisabled: true, dialog: [...this.state.dialog, { message: response.inputTranscript, from: 'me' }, response && { from: 'bot', message: response.message }], inputText: '' }, () => { this.doneSpeakingHandler(); }) this.listItemsRef.current.scrollTop = this.listItemsRef.current.scrollHeight; } doneSpeakingHandler() { if (!this.state.continueConversation) { return; } if (this.state.lexResponse.contentType === 'audio/mpeg') { 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, }, () => { 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, }, () => { audioControl.clear(); }); } listItems() { return this.state.dialog.map((m, i) => { if (m.from === 'me') { return <div key={i} style={styles.itemMe}>{m.message}</div>; } else if (m.from === 'system') { return <div key={i} style={styles.itemBot}>{m.message}</div>; } else { return <div key={i} style={styles.itemBot}>{m.message}</div>; } }); } 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' }, ] }, resolve)); if (!Interactions || typeof Interactions.send !== 'function') { throw new Error('No Interactions module found, please ensure @aws-amplify/interactions is imported'); } const response = await Interactions.send(this.props.botName, this.state.inputText); this.setState({ dialog: [...this.state.dialog, response && { from: 'bot', message: response.message }], inputText: '' }); this.listItemsRef.current.scrollTop = this.listItemsRef.current.scrollHeight; } async changeInputText(event) { await this.setState({ inputText: event.target.value }); } getOnComplete(fn) { 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} = 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'); } Interactions.onComplete(botName, this.getOnComplete(onComplete, this)); } } 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'); } Interactions.onComplete(botName, this.getOnComplete(onComplete, this)); } } render() { const { title, theme, onComplete } = this.props; return ( <FormSection theme={theme}> {title && <SectionHeader theme={theme}>{I18n.get(title)}</SectionHeader>} <SectionBody theme={theme}> <div ref={this.listItemsRef} style={styles.list}>{this.listItems()}</div> </SectionBody> <SectionFooter theme={theme}> <ChatBotInputs micText={this.state.micText} voiceEnabled={this.props.voiceEnabled} textEnabled={this.props.textEnabled} styles={styles} onChange={this.changeInputText} inputText={this.state.inputText} onSubmit={this.submit} inputDisabled={this.state.inputDisabled} micButtonDisabled={this.state.micButtonDisabled} handleMicButton={this.micButtonHandler} currentVoiceState={this.state.currentVoiceState}> </ChatBotInputs> </SectionFooter> </FormSection> ); } } function ChatBotTextInput(props) { const styles=props.styles const onChange=props.onChange const inputText=props.inputText const inputDisabled=props.inputDisabled const currentVoiceState=props.currentVoiceState return( <input style={styles.textInput} type='text' placeholder={I18n.get(currentVoiceState.MESSAGE)} onChange={onChange} value={inputText} disabled={inputDisabled}> </input> ) } 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( <button style={styles.mic} disabled={micButtonDisabled} onClick={handleMicButton}> {micText} </button> ) } function ChatBotTextButton(props) { const textEnabled = props.textEnabled; const styles = props.styles; const inputDisabled = props.inputDisabled; if (!textEnabled) { return null; } return( <button type="submit" style={styles.button} disabled={inputDisabled}> {I18n.get('Send')} </button> ) } function ChatBotInputs(props) { const voiceEnabled = props.voiceEnabled; const textEnabled = props.textEnabled; const styles = props.styles; const onChange = props.onChange; const 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(<div>No Chatbot inputs enabled. Set at least one of voiceEnabled or textEnabled in the props. </div>) } return ( <form onSubmit={onSubmit}> <ChatBotTextInput onSubmit={onSubmit} styles={styles} type='text' currentVoiceState={currentVoiceState} onChange={onChange} inputText={inputText} inputDisabled={inputDisabled} /> <ChatBotTextButton onSubmit={onSubmit} type="submit" styles={styles} inputDisabled={inputDisabled} textEnabled={textEnabled} /> <ChatBotMicButton styles={styles} micButtonDisabled={micButtonDisabled} handleMicButton={handleMicButton} micText={micText} voiceEnabled={voiceEnabled} /> </form>); } ChatBot.defaultProps = { title: '', botName: '', onComplete: undefined, clearOnComplete: false, voiceConfig: defaultVoiceConfig, conversationModeOn: false, voiceEnabled: false, textEnabled: true }; export default ChatBot;