import React, { Component } from 'react'; import { View, TextInput, Text, KeyboardAvoidingView, ScrollView } from 'react-native'; import { Interactions } from '@aws-amplify/interactions'; import { I18n } from 'aws-amplify'; import { AmplifyButton } from '../AmplifyUI'; import { ConsoleLogger as Logger } from '@aws-amplify/core'; var Voice; var RNFS; var Sound; var Buffer = require('buffer/').Buffer; const logger = new Logger('ChatBot'); const styles = { container: { flex: 1, flexDirection: 'column', backgroundColor: '#fff', alignItems: 'center', alignSelf: 'stretch', justifyContent: 'center', }, list: { flex: 1, flexDirection: 'column', alignSelf: 'stretch', padding: 5, }, itemMe: { textAlign: 'right', alignSelf: 'flex-end', padding: 8, margin: 8, backgroundColor: '#CCCCCC', borderRadius: 15, overflow: 'hidden', }, itemBot: { textAlign: 'left', alignSelf: 'flex-start', padding: 8, margin: 8, color: 'white', backgroundColor: '#0099FF', borderRadius: 15, overflow: 'hidden', }, inputContainer: { flexDirection: 'row', }, textInput: { flex: 1, }, buttonMic: { backgroundColor: '#ffc266', }, }; const STATES = { INITIAL: 'INITIAL', LISTENING: 'LISTENING', SENDING: 'SENDING', SPEAKING: 'SPEAKING', }; const MIC_BUTTON_TEXT = { PASSIVE: '🎤', RECORDING: '🔴', }; let timer = null; interface IChatBotProps { botName?: string; clearOnComplete?: boolean; conversationModeOn?: boolean; onComplete?: Function; styles?: any; textEnabled?: boolean; voiceEnabled?: boolean; voiceLibs?: { Voice: any; Sound: any; RNFS: any }; welcomeMessage?: string; } interface IChatBotState { conversationOngoing: boolean; currentConversationState?: string; dialog: any[]; error?: string; inputText: string; inputEditable: boolean; micText: string; silenceDelay?: number; voice: boolean; } export class ChatBot extends Component { listItemsRef: React.RefObject; constructor(props) { super(props); this.state = { dialog: [ { message: this.props.welcomeMessage || 'Welcome to Lex', from: 'system', }, ], inputText: '', inputEditable: true, micText: MIC_BUTTON_TEXT.PASSIVE, voice: false, conversationOngoing: false, }; this.listItems = this.listItems.bind(this); this.submit = this.submit.bind(this); this.listItemsRef = React.createRef(); this.reset = this.reset.bind(this); this.startRecognizing = this.startRecognizing.bind(this); this.handleMicButton = this.handleMicButton.bind(this); if (this.props.voiceEnabled) { if (!this.props.voiceLibs) { throw new Error('Missing voiceLibs for voice interactions'); } Voice = this.props.voiceLibs.Voice; Sound = this.props.voiceLibs.Sound; RNFS = this.props.voiceLibs.RNFS; if ( !Voice || typeof Voice.start !== 'function' || typeof Voice.stop !== 'function' || typeof Voice.isRecognizing !== 'function' ) { throw new Error('Missing react-native-voice'); } if (!Sound) { throw new Error('Missing react-native-sound'); } if ( !RNFS || typeof RNFS.exists !== 'function' || typeof RNFS.unlink !== 'function' || typeof RNFS.writeFile !== 'function' ) { throw new Error('Missing react-native-fs'); } Voice.onSpeechStart = this.onSpeechStart.bind(this); Voice.onSpeechEnd = this.onSpeechEnd.bind(this); Voice.onSpeechError = this.onSpeechError.bind(this); Voice.onSpeechResults = this.onSpeechResults.bind(this); } } listItems() { const { styles: overrideStyles } = this.props; return this.state.dialog.map((m, i) => { if (m.from === 'me') { return ( {m.message} ); } else if (m.from === 'system') { return ( {m.message} ); } else { return ( {m.message} ); } }); } async submit(voiceResponse) { if (!this.state.inputText) { return; } await new Promise((resolve) => this.setState( { dialog: [...this.state.dialog, { message: this.state.inputText, from: 'me' }], }, resolve ) ); let response; if (voiceResponse === true) { const interactionsMessage = { content: this.state.inputText, options: { messageType: 'text', }, }; response = await Interactions.send(this.props.botName, interactionsMessage); } else { response = await Interactions.send(this.props.botName, this.state.inputText); } this.setState( { dialog: [ ...this.state.dialog, response && response.message && { from: 'bot', message: response.message, }, ].filter(Boolean), inputText: '', inputEditable: true, micText: MIC_BUTTON_TEXT.PASSIVE, }, () => { setTimeout(() => { this.listItemsRef.current.scrollToEnd(); }, 50); } ); if (this.state.voice) { this.setState({ voice: false, }); const path = `${RNFS.DocumentDirectoryPath}/responseAudio.mp3`; const data = Buffer.from(response.audioStream).toString('base64'); await RNFS.writeFile(path, data, 'base64'); const speech = new Sound(path, '', async (err) => { if (!err) { speech.play(async () => { speech.release(); RNFS.exists(path).then((res) => { if (res) { RNFS.unlink(path); } }); if (response.dialogState === 'ElicitSlot' && this.props.conversationModeOn) { await this.startRecognizing(); } }); } else { logger.error(err); } }); } } 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), }, () => { setTimeout(() => { this.listItemsRef.current.scrollToEnd(); }, 50); } ); }; } componentDidMount() { const { onComplete, botName } = this.props; if (onComplete && botName) { // @ts-ignore Interactions.onComplete(botName, this.getOnComplete(onComplete, this)); } } componentDidUpdate(prevProps) { const { onComplete, botName } = this.props; if (botName !== prevProps.botName || onComplete !== prevProps.onComplete) { // @ts-ignore Interactions.onComplete(botName, this.getOnComplete(onComplete, this)); } } onSpeechStart(e) { this.setState({ currentConversationState: STATES.LISTENING, }); } async onSpeechEnd(e) { timer = null; this.setState({ currentConversationState: STATES.SENDING, }); await this.submit(true); } onSpeechError(e) { logger.error(e); this.setState({ error: JSON.stringify(e.error), }); } onSpeechResults(e) { this.setState({ inputText: e.value.join(' '), }); if (timer !== null) { clearTimeout(timer); } timer = setTimeout(async () => { await Voice.stop(); }, this.state.silenceDelay); } async startRecognizing() { this.setState({ inputText: 'Speak into the mic...', inputEditable: false, micText: MIC_BUTTON_TEXT.RECORDING, voice: true, }); if (this.props.conversationModeOn) { this.setState({ conversationOngoing: true, }); } try { await Voice.start('en-US'); } catch (e) { logger.error(e); } } async handleMicButton() { if (this.state.conversationOngoing || (await Voice.isRecognizing())) { await this.reset(); } else { await this.startRecognizing(); } } async reset() { this.setState({ inputText: '', inputEditable: true, micText: MIC_BUTTON_TEXT.PASSIVE, voice: false, conversationOngoing: false, }); await Voice.stop(); } render() { const { styles: overrideStyles } = this.props; return ( {this.listItems()} this.setState({ inputText })} inputText={this.state.inputText} onSubmitEditing={this.submit} editable={this.state.inputEditable} handleMicButton={this.handleMicButton} submit={this.submit} > ); } } function ChatBotInputs(props) { const voiceEnabled = props.voiceEnabled; const textEnabled = props.textEnabled; const styles = props.styles; const overrideStyles = props.overrideStyles; const onChangeText = props.onChangeText; const inputText = props.inputText; const onSubmitEditing = props.onSubmitEditing; let editable = props.editable; const handleMicButton = props.handleMicButton; const micText = props.micText; const submit = props.submit; let placeholder; if (voiceEnabled && textEnabled) { // @ts-ignore placeholder = 'Type your message or tap 🎤'; } if (voiceEnabled && !textEnabled) { // @ts-ignore placeholder = 'Tap the mic button'; editable = false; } if (!voiceEnabled && textEnabled) { // @ts-ignore placeholder = 'Type your message here'; } if (!voiceEnabled && !textEnabled) { return No Chatbot inputs enabled. Set at least one of voiceEnabled or textEnabled in the props. ; } return ( ); } function ChatBotTextInput(props) { const styles = props.styles; const overrideStyles = props.overrideStyles; const onChangeText = props.onChangeText; const inputText = props.inputText; const onSubmitEditing = props.onSubmitEditing; const editable = props.editable; const placeholder = props.placeholder; return ( ); } function ChatBotTextButton(props) { const textEnabled = props.textEnabled; const styles = props.styles; const overrideStyles = props.overrideStyles; const submit = props.submit; if (!textEnabled) { return null; } return ( ); } function ChatBotMicButton(props) { const voiceEnabled = props.voiceEnabled; const styles = props.styles; const overrideStyles = props.overrideStyles; const handleMicButton = props.handleMicButton; const micText = props.micText; if (!voiceEnabled) { return null; } return ( ); } // @ts-ignore ChatBot.defaultProps = { botName: undefined, onComplete: undefined, clearOnComplete: false, styles: {}, silenceDelay: 1000, conversationModeOn: false, voiceEnabled: false, textEnabled: true, }; export default ChatBot;