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
JSX
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;