Skip to content

Commit 467f881

Browse files
Merge pull request LucasBassetti#152 from puemos/118-speak
the chat can speak
2 parents 518c68c + 910121e commit 467f881

File tree

9 files changed

+2770
-2032
lines changed

9 files changed

+2770
-2032
lines changed

dist/react-simple-chatbot.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/ChatBot.jsx

Lines changed: 95 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import _ from 'lodash';
21
import React, { Component } from 'react';
32
import PropTypes from 'prop-types';
43
import Random from 'random-id';
@@ -19,6 +18,7 @@ import {
1918
import Recognition from './recognition';
2019
import { ChatIcon, CloseIcon, SubmitIcon, MicIcon } from './icons';
2120
import { isMobile } from './utils';
21+
import { speakFn } from './speechSynthesis';
2222

2323
class ChatBot extends Component {
2424
/* istanbul ignore next */
@@ -39,7 +39,7 @@ class ChatBot extends Component {
3939
recognitionEnable: props.recognitionEnable && Recognition.isSupported(),
4040
defaultUserSettings: {},
4141
};
42-
42+
this.speak = speakFn(props.speechSynthesis);
4343
this.renderStep = this.renderStep.bind(this);
4444
this.getTriggeredStep = this.getTriggeredStep.bind(this);
4545
this.generateRenderedStepsById = this.generateRenderedStepsById.bind(this);
@@ -95,24 +95,22 @@ class ChatBot extends Component {
9595
steps[firstStep.id].message = firstStep.message;
9696
}
9797

98-
const {
99-
currentStep,
100-
previousStep,
101-
previousSteps,
102-
renderedSteps,
103-
} = storage.getData({
104-
cacheName,
105-
cache,
106-
firstStep,
107-
steps,
108-
}, () => {
109-
// focus input if last step cached is a user step
110-
this.setState({ disabled: false }, () => {
111-
if (enableMobileAutoFocus || !isMobile()) {
112-
this.input.focus();
113-
}
114-
});
115-
});
98+
const { currentStep, previousStep, previousSteps, renderedSteps } = storage.getData(
99+
{
100+
cacheName,
101+
cache,
102+
firstStep,
103+
steps,
104+
},
105+
() => {
106+
// focus input if last step cached is a user step
107+
this.setState({ disabled: false }, () => {
108+
if (enableMobileAutoFocus || !isMobile()) {
109+
this.input.focus();
110+
}
111+
});
112+
},
113+
);
116114

117115
this.setState({
118116
currentStep,
@@ -210,12 +208,7 @@ class ChatBot extends Component {
210208

211209
triggerNextStep(data) {
212210
const { enableMobileAutoFocus } = this.props;
213-
const {
214-
defaultUserSettings,
215-
previousSteps,
216-
renderedSteps,
217-
steps,
218-
} = this.state;
211+
const { defaultUserSettings, previousSteps, renderedSteps, steps } = this.state;
219212
let { currentStep, previousStep } = this.state;
220213
const isEnd = currentStep.end;
221214

@@ -331,7 +324,10 @@ class ChatBot extends Component {
331324
this.props.handleEnd({ renderedSteps, steps, values });
332325
}
333326
}
334-
327+
isInputValueEmpty() {
328+
const { inputValue } = this.state;
329+
return Boolean(inputValue) && inputValue.length > 0;
330+
}
335331
isLastPosition(step) {
336332
const { renderedSteps } = this.state;
337333
const length = renderedSteps.length;
@@ -378,8 +374,8 @@ class ChatBot extends Component {
378374
}
379375

380376
handleSubmitButton() {
381-
const { inputValue, speaking, recognitionEnable } = this.state;
382-
if ((_.isEmpty(inputValue) || speaking) && recognitionEnable) {
377+
const { speaking, recognitionEnable } = this.state;
378+
if ((this.isInputValueEmpty() || speaking) && recognitionEnable) {
383379
this.recognition.speak();
384380
if (!speaking) {
385381
this.setState({ speaking: true });
@@ -406,15 +402,18 @@ class ChatBot extends Component {
406402
renderedSteps.push(currentStep);
407403
previousSteps.push(currentStep);
408404

409-
this.setState({
410-
currentStep,
411-
renderedSteps,
412-
previousSteps,
413-
disabled: true,
414-
inputValue: '',
415-
}, () => {
416-
this.input.blur();
417-
});
405+
this.setState(
406+
{
407+
currentStep,
408+
renderedSteps,
409+
previousSteps,
410+
disabled: true,
411+
inputValue: '',
412+
},
413+
() => {
414+
this.input.blur();
415+
},
416+
);
418417
}
419418
}
420419

@@ -425,23 +424,29 @@ class ChatBot extends Component {
425424
const value = inputValue;
426425

427426
if (typeof result !== 'boolean' || !result) {
428-
this.setState({
429-
inputValue: result.toString(),
430-
inputInvalid: true,
431-
disabled: true,
432-
}, () => {
433-
setTimeout(() => {
434-
this.setState({
435-
inputValue: value,
436-
inputInvalid: false,
437-
disabled: false,
438-
}, () => {
439-
if (enableMobileAutoFocus || !isMobile()) {
440-
this.input.focus();
441-
}
442-
});
443-
}, 2000);
444-
});
427+
this.setState(
428+
{
429+
inputValue: result.toString(),
430+
inputInvalid: true,
431+
disabled: true,
432+
},
433+
() => {
434+
setTimeout(() => {
435+
this.setState(
436+
{
437+
inputValue: value,
438+
inputInvalid: false,
439+
disabled: false,
440+
},
441+
() => {
442+
if (enableMobileAutoFocus || !isMobile()) {
443+
this.input.focus();
444+
}
445+
},
446+
);
447+
}, 2000);
448+
},
449+
);
445450

446451
return true;
447452
}
@@ -466,6 +471,7 @@ class ChatBot extends Component {
466471
customStyle,
467472
hideBotAvatar,
468473
hideUserAvatar,
474+
speechSynthesis,
469475
} = this.props;
470476
const { options, component, asMessage } = step;
471477
const steps = this.generateRenderedStepsById();
@@ -475,10 +481,12 @@ class ChatBot extends Component {
475481
return (
476482
<CustomStep
477483
key={index}
484+
speak={this.speak}
478485
step={step}
479486
steps={steps}
480487
style={customStyle}
481488
previousStep={previousStep}
489+
previousValue={previousStep.value}
482490
triggerNextStep={this.triggerNextStep}
483491
/>
484492
);
@@ -489,6 +497,8 @@ class ChatBot extends Component {
489497
<OptionsStep
490498
key={index}
491499
step={step}
500+
speak={this.speak}
501+
previousValue={previousStep.value}
492502
triggerNextStep={this.triggerNextStep}
493503
bubbleOptionStyle={bubbleOptionStyle}
494504
/>
@@ -500,13 +510,15 @@ class ChatBot extends Component {
500510
key={index}
501511
step={step}
502512
steps={steps}
513+
speak={this.speak}
503514
previousStep={previousStep}
504515
previousValue={previousStep.value}
505516
triggerNextStep={this.triggerNextStep}
506517
avatarStyle={avatarStyle}
507518
bubbleStyle={bubbleStyle}
508519
hideBotAvatar={hideBotAvatar}
509520
hideUserAvatar={hideUserAvatar}
521+
speechSynthesis={speechSynthesis}
510522
isFirst={this.isFirstPosition(step)}
511523
isLast={this.isLastPosition(step)}
512524
/>
@@ -556,10 +568,11 @@ class ChatBot extends Component {
556568
);
557569

558570
const icon =
559-
(_.isEmpty(inputValue) || speaking) && recognitionEnable ? <MicIcon /> : <SubmitIcon />;
571+
(this.isInputValueEmpty() || speaking) && recognitionEnable ? <MicIcon /> : <SubmitIcon />;
560572

561-
const inputPlaceholder = speaking ? recognitionPlaceholder :
562-
currentStep.placeholder || placeholder;
573+
const inputPlaceholder = speaking
574+
? recognitionPlaceholder
575+
: currentStep.placeholder || placeholder;
563576

564577
const inputAttributesOverride = currentStep.inputAttributes || inputAttributes;
565578

@@ -593,7 +606,7 @@ class ChatBot extends Component {
593606
height={height}
594607
hideInput={currentStep.hideInput}
595608
>
596-
{_.map(renderedSteps, this.renderStep)}
609+
{Object.values(renderedSteps).map(this.renderStep)}
597610
</Content>
598611
<Footer className="rsc-footer" style={footerStyle}>
599612
{!currentStep.hideInput && (
@@ -613,18 +626,19 @@ class ChatBot extends Component {
613626
{...inputAttributesOverride}
614627
/>
615628
)}
616-
{!currentStep.hideInput && !hideSubmitButton && (
617-
<SubmitButton
618-
className="rsc-submit-button"
619-
style={submitButtonStyle}
620-
onClick={this.handleSubmitButton}
621-
invalid={inputInvalid}
622-
disabled={disabled}
623-
speaking={speaking}
624-
>
625-
{icon}
626-
</SubmitButton>
627-
)}
629+
{!currentStep.hideInput &&
630+
!hideSubmitButton && (
631+
<SubmitButton
632+
className="rsc-submit-button"
633+
style={submitButtonStyle}
634+
onClick={this.handleSubmitButton}
635+
invalid={inputInvalid}
636+
disabled={disabled}
637+
speaking={speaking}
638+
>
639+
{icon}
640+
</SubmitButton>
641+
)}
628642
</Footer>
629643
</ChatBotContainer>
630644
</div>
@@ -670,6 +684,11 @@ ChatBot.propTypes = {
670684
userDelay: PropTypes.number,
671685
width: PropTypes.string,
672686
height: PropTypes.string,
687+
speechSynthesis: PropTypes.shape({
688+
enable: PropTypes.bool,
689+
lang: PropTypes.string,
690+
voice: PropTypes.instanceOf(window.SpeechSynthesisVoice),
691+
}),
673692
};
674693

675694
ChatBot.defaultProps = {
@@ -701,6 +720,11 @@ ChatBot.defaultProps = {
701720
recognitionEnable: false,
702721
recognitionLang: 'en',
703722
recognitionPlaceholder: 'Listening ...',
723+
speechSynthesis: {
724+
enable: false,
725+
lang: 'en',
726+
voice: null,
727+
},
704728
style: {},
705729
submitButtonStyle: {},
706730
toggleFloating: undefined,

lib/speechSynthesis.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { isString } from './utils';
2+
3+
export const getSpeakText = (step) => {
4+
const { message, metadata = {} } = step;
5+
if (isString(metadata.speak)) {
6+
return metadata.speak;
7+
}
8+
if (isString(message)) {
9+
return message;
10+
}
11+
return '';
12+
};
13+
14+
export const speakFn = speechSynthesisOptions => (step, previousValue) => {
15+
const { lang, voice, enable } = speechSynthesisOptions;
16+
const { user } = step;
17+
18+
if (!window.SpeechSynthesisUtterance || !window.speechSynthesis) {
19+
return;
20+
}
21+
if (user) {
22+
return;
23+
}
24+
if (!enable) {
25+
return;
26+
}
27+
const text = getSpeakText(step);
28+
const msg = new window.SpeechSynthesisUtterance();
29+
msg.text = text.replace(/{previousValue}/g, previousValue);
30+
msg.lang = lang;
31+
msg.voice = voice;
32+
window.speechSynthesis.speak(msg);
33+
};

lib/steps_components/custom/CustomStep.jsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ class CustomStep extends Component {
1616
}
1717

1818
componentDidMount() {
19-
const { step } = this.props;
19+
const { speak, step, previousValue } = this.props;
2020
const { delay, waitAction } = step;
2121

2222
setTimeout(() => {
2323
this.setState({ loading: false }, () => {
2424
if (!waitAction && !step.rendered) {
2525
this.props.triggerNextStep();
2626
}
27+
speak(step, previousValue);
2728
});
2829
}, delay);
2930
}
@@ -44,15 +45,8 @@ class CustomStep extends Component {
4445
const { style } = this.props;
4546

4647
return (
47-
<CustomStepContainer
48-
className="rsc-cs"
49-
style={style}
50-
>
51-
{
52-
loading ? (
53-
<Loading />
54-
) : this.renderComponent()
55-
}
48+
<CustomStepContainer className="rsc-cs" style={style}>
49+
{loading ? <Loading /> : this.renderComponent()}
5650
</CustomStepContainer>
5751
);
5852
}
@@ -64,6 +58,12 @@ CustomStep.propTypes = {
6458
style: PropTypes.object.isRequired,
6559
previousStep: PropTypes.object.isRequired,
6660
triggerNextStep: PropTypes.func.isRequired,
61+
previousValue: PropTypes.any,
62+
speak: PropTypes.func,
63+
};
64+
CustomStep.defaultProps = {
65+
previousValue: '',
66+
speak: () => {},
6767
};
6868

6969
export default CustomStep;

0 commit comments

Comments
 (0)