1
/*
2
 * Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
5
 * the License. A copy of the License is located at
6
 *
7
 *     http://aws.amazon.com/apache2.0/
8
 *
9
 * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10
 * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
11
 * and limitations under the License.
12
 */
13

14
<template>
15
  <div class="amplify-interactions">
16
    <div class="amplify-interactions-container">
17
      <div class="amplify-form-container">
18
        <div class="amplify-form-row">
19
          <div class="amplify-interactions-conversation">
20
            <div v-for="message in messages" v-bind:key="message.meSentTime">
21
              <div class="amplify-interactions-input">{{message.me}}</div>
22
              <div class="amplify-interactions-input-timestamp">{{message.meSentTime}}</div>
23
              <div class="amplify-interactions-response">{{message.bot}}</div>
24
              <div class="amplify-interactions-response-timestamp">{{message.botSentTime}}</div>
25
            </div>
26
          </div>
27
        </div>
28
        <div class="amplify-interactions-actions">
29
          <input
30
            type="text"
31
            class="amplify-form-input"
32
            :placeholder="currentVoiceState"
33
            v-model="inputText"
34
            v-on:keyup="keymonitor"
35
            :disabled="inputDisabled"
36
            v-if="options.textEnabled"
37
          >
38
          <input
39
            type="text"
40
            class="amplify-form-input"
41
            :placeholder="currentVoiceState"
42
            :disabled="!options.textEnabled"
43
            v-if="!options.textEnabled"
44
          >
45
          <button
46
            :disabled="micButtonDisabled"
47
            v-if="options.voiceEnabled"
48
            class="amplify-mic-button"
49
            @click="micButtonHandler()"
50
          >{{this.micText}}</button>
51
          <button
52
            id="interactions-submit-button"
53
            v-if="options.textEnabled"
54
            class="amplify-interactions-button"
55
            @click="onSubmit(inputText)"
56
            :disabled="inputDisabled"
57
          ></button>
58
        </div>
59
      </div>
60
    </div>
61
    <div class="error" v-if="error">{{ error }}</div>
62
  </div>
63
</template>
64

65
<script>
66 1
import AmplifyEventBus from "../../events/AmplifyEventBus";
67

68
let audioControl;
69

70 1
const STATES = {
71
  INITIAL: { MESSAGE: "Type your message or click  🎀", ICON: "🎀" },
72
  LISTENING: { MESSAGE: "Listening... click πŸ”΄ again to cancel", ICON: "πŸ”΄" },
73
  SENDING: { MESSAGE: "Please wait...", ICON: "πŸ”Š" },
74
  SPEAKING: { MESSAGE: "Speaking...", ICON: "..." }
75
};
76 1
const defaultVoiceConfig = {
77
  silenceDetectionConfig: {
78
    time: 2000,
79
    amplitude: 0.2
80
  }
81
};
82

83
export default {
84
  name: "Chatbot",
85
  props: ["chatbotConfig"],
86
  STATES: STATES,
87
  defaultVoiceConfig: defaultVoiceConfig,
88
  audioControl: audioControl,
89
  data() {
90 1
    return {
91
      inputText: "",
92
      error: "",
93
      messages: [],
94
      logger: {},
95
      currentVoiceState: STATES.INITIAL.MESSAGE,
96
      inputDisabled: false,
97
      micText: STATES.INITIAL.ICON,
98
      continueConversation: false,
99
      micButtonDisabled: false
100
    };
101
  },
102
  computed: {
103
    options() {
104 1
      const defaults = {
105
        clearComplete: true,
106
        botTitle: "Chatbot",
107
        conversationModeOn: false,
108
        voiceConfig: defaultVoiceConfig,
109
        voiceEnabled: false,
110
        textEnabled: true
111
      };
112 1
      return Object.assign(defaults, this.chatbotConfig || {});
113
    }
114
  },
115
  mounted() {
116 1
    this.logger = new this.$Amplify.Logger(this.$options.name);
117 1
    if (!this.options.bot) {
118 1
      this.setError("Bot not provided.");
119
    }
120 1
    if (this.options.voiceEnabled) {
121 0
      require("./aws-lex-audio.js");
122 0
      audioControl = new global.LexAudio.audioControl();
123
    }
124 1
    if (!this.options.textEnabled && this.options.voiceEnabled) {
125 0
      STATES.INITIAL.MESSAGE = "Click the mic button";
126 0
      this.currentVoiceState = STATES.INITIAL.MESSAGE;
127
    }
128 1
    if (this.options.textEnabled && !this.options.voiceEnabled) {
129 1
      STATES.INITIAL.MESSAGE = "Type a message";
130 1
      this.currentVoiceState = STATES.INITIAL.MESSAGE;
131
    }
132

133 1
    this.$Amplify.Interactions.onComplete(
134
      this.options.bot,
135
      this.performOnComplete
136
    );
137
  },
138
  methods: {
139
    performOnComplete(evt) {
140 1
      AmplifyEventBus.$emit("chatComplete", this.options.botTitle);
141 1
      if (this.options.clearComplete) {
142 1
        this.messages = [];
143
      }
144
    },
145
    keymonitor(event) {
146 1
      if (event.key.toLowerCase() == "enter") {
147 1
        this.onSubmit(this.inputText);
148
      }
149
    },
150
    onSubmit(e) {
151 1
      if (!this.inputText) {
152 1
        return;
153
      }
154 1
      let message = {
155
        me: this.inputText,
156
        meSentTime: new Date().toLocaleTimeString(),
157
        bot: "",
158
        botSentTime: ""
159
      };
160 1
      this.$Amplify.Interactions.send(this.options.bot, this.inputText)
161
        .then(response => {
162 1
          AmplifyEventBus.$emit("chatResponse", response);
163 1
          this.inputText = "";
164 1
          if (response.message) {
165 0
            message.bot = response.message;
166 0
            message.botSentTime = new Date().toLocaleTimeString();
167 0
            this.messages.push(message);
168
          }
169 0
        })
170
        .catch(e => this.setError(e));
171
    },
172
    setError: function(e) {
173 1
      this.error = this.$Amplify.I18n.get(e.message || e);
174 1
      this.logger.error(this.error);
175
    },
176
    async micButtonHandler() {
177 1
      if (this.continueConversation) {
178 0
        this.reset();
179
      } else {
180 0
        this.inputDisabled = true;
181 0
        this.continueConversation = true;
182 0
        this.currentVoiceState = STATES.LISTENING.MESSAGE;
183 0
        this.micText = STATES.LISTENING.ICON;
184 0
        this.micButtonDisabled = false;
185 0
        audioControl.startRecording(
186
          this.onSilenceHandler,
187
          null,
188
          this.options.voiceConfig.silenceDetectionConfig
189
        );
190
      }
191
    },
192
    onSilenceHandler() {
193 0
      audioControl.stopRecording();
194 1
      if (!this.continueConversation) {
195 0
        return;
196
      }
197

198 0
      audioControl.exportWAV(blob => {
199 0
        this.currentVoiceState = STATES.SENDING.MESSAGE;
200 0
        this.audioInput = blob;
201 0
        this.micText = STATES.SENDING.ICON;
202 0
        this.micButtonDisabled = true;
203 0
        this.lexResponseHandler();
204
      });
205
    },
206
    async lexResponseHandler() {
207 1
      if (!this.continueConversation) {
208 0
        return;
209
      }
210

211 0
      const interactionsMessage = {
212
        content: this.audioInput,
213
        options: {
214
          messageType: "voice"
215
        }
216
      };
217

218 0
      const response = await this.$Amplify.Interactions.send(
219
        this.options.bot,
220
        interactionsMessage
221
      );
222

223 0
      this.lexResponse = response;
224 0
      this.currentVoiceState = STATES.SPEAKING.MESSAGE;
225 0
      this.micText = STATES.SPEAKING.ICON;
226 0
      this.micButtonDisabled = true;
227

228 0
      let message = {
229
        me: this.lexResponse.inputTranscript,
230
        meSentTime: new Date().toLocaleTimeString(),
231
        bot: "",
232
        botSentTime: ""
233
      };
234

235 0
      this.inputText = "";
236 1
      if (response.message) {
237 0
        message.bot = response.message;
238 0
        message.botSentTime = new Date().toLocaleTimeString();
239 0
        this.messages.push(message);
240
      }
241

242 0
      this.inputText = "";
243

244 0
      this.doneSpeakingHandler();
245
    },
246
    doneSpeakingHandler() {
247 1
      if (!this.continueConversation) {
248 0
        return;
249
      }
250 1
      if (this.lexResponse.contentType === "audio/mpeg") {
251 0
        audioControl.play(this.lexResponse.audioStream, () => {
252 1
          if (
253
            this.lexResponse.dialogState === "ReadyForFulfillment" ||
254
            this.lexResponse.dialogState === "Fulfilled" ||
255
            this.lexResponse.dialogState === "Failed" ||
256
            !this.options.conversationModeOn
257
          ) {
258 0
            this.inputDisabled = false;
259 0
            this.currentVoiceState = STATES.INITIAL.MESSAGE;
260 0
            this.micText = STATES.INITIAL.ICON;
261 0
            this.micButtonDisabled = false;
262 0
            this.continueConversation = false;
263
          } else {
264 0
            this.currentVoiceState = STATES.LISTENING.MESSAGE;
265 0
            this.micText = STATES.LISTENING.ICON;
266 0
            this.micButtonDisabled = false;
267 0
            audioControl.startRecording(
268
              this.onSilenceHandler,
269
              null,
270
              this.options.voiceConfig.silenceDetectionConfig
271
            );
272
          }
273
        });
274
      } else {
275 0
        this.inputDisabled = false;
276 0
        this.currentVoiceState = STATES.INITIAL.MESSAGE;
277 0
        this.micText = STATES.INITIAL.ICON;
278 0
        this.micButtonDisabled = false;
279 0
        this.continueConversation = false;
280
      }
281
    },
282
    reset() {
283 0
      audioControl.clear();
284 0
      this.inputText = "";
285 0
      this.currentVoiceState = STATES.INITIAL.MESSAGE;
286 0
      this.inputDisabled = false;
287 0
      this.micText = STATES.INITIAL.ICON;
288 0
      this.continueConversation = false;
289 0
      this.micButtonDisabled = false;
290
    }
291
  }
292
};
293
</script>
294

295
<style scoped>
296
.amplify-interactions {
297
  width: var(--component-width-desktop);
298
  margin: 1em auto;
299
  border-radius: 6px;
300
  background-color: var(--color-white);
301
  box-shadow: var(--box-shadow);
302
}
303

304
.amplify-interactions-container {
305
  width: 400px;
306
  margin: 0 auto;
307
  padding: 1em;
308
}
309

310
.amplify-interactions-button {
311
  background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAApCAYAAABHomvIAAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAZ25vbWUtc2NyZWVuc2hvdO8Dvz4AAAG2SURBVFiF7dfPKwRhHMfx9+xqhaItIg5KikgOlER2TzhQ4i+QwtndQe4O4uhCSQ57sC6SkotfR1uK2nWgrGJr8yvsjsO2bWtb+8w+88zOYT6nmWfmeebV95lfj6bruo6N4yo1oFAcoGwcoGwcoGwcoGxsDyyz5CqxawhtwPsTNA1C5wxobqGumvJv8d0+HM5B4jvT1jICw1tC3dVO8ds9VNaD25PdHj6A6LnQEOqAkSBs90HsBsZ2wVOVfTweFhpGDTAShMPZ1LQeL+QiNQ1qu0sETOOSidS+ruciu6bB2yE0nLkPyV9c1pU08K9AQx9Ut4AmVhvzKvgfDlKVfDyFmlZhHJgFLIQDaJ+CoVXDQ8sDRXG+dUOVS0cOqBgHMkARXJscDooFiuL8cjgoBmghDoy+B+MR2BmA5I8lODBSQT0JiU/oX8x/jsk4MAJ8CcHeBDQNwcCSJTgwAoxewkcMgpO5SEU4MHIPHs3DbSC1XeGF8QA8nMDzFfjWlODAyC9/9Cyz/fUKF8swvAkuT/4+JkQc2DwKVY3Q0At1PeAuV8jKRP2aRDK2X3Y6QNk4QNk4QNnYHvgLzPueuQw6nCEAAAAASUVORK5CYII=")
312
    center no-repeat var(--color-white);
313
  border: none;
314
  cursor: pointer;
315
  width: 32px;
316
}
317

318
.amplify-mic-button {
319
  border: none;
320
  cursor: pointer;
321
  width: 32px;
322
}
323

324
.amplify-form-input {
325
  width: 100%;
326
}
327

328
.amplify-interactions-actions {
329
  display: flex;
330
  border-top: var(--input-border);
331
  margin-bottom: -1em;
332
  margin-left: -1.9em;
333
  margin-right: -1.9em;
334
}
335

336
.amplify-interactions-actions > input[type="text"] {
337
  border: none;
338
  margin-top: 0px;
339
  margin-bottom: 0px;
340
  margin-left: 0px;
341
}
342

343
.amplify-interactions-actions > input[type="text"]:focus {
344
  border: 0px solid var(--color-white) !important;
345
}
346

347
.amplify-interactions-conversation {
348
  margin: 1em;
349
}
350

351
.amplify-interactions-input {
352
  padding: 1em;
353
  margin: 1em;
354
  width: 75%;
355
  margin-left: 5em;
356
  border-radius: 20px 20px 0 20px;
357
  background-color: #009ecf;
358
  box-shadow: 1px 2px 4px 0 rgba(0, 0, 0, 0.1);
359
  color: var(--color-white);
360
  font-size: 13px;
361
  line-height: 16px;
362
}
363

364
.amplify-interactions-input-timestamp {
365
  color: #828282;
366
  font-size: 10px;
367
  letter-spacing: 0.5px;
368
  line-height: 16px;
369
  text-align: right;
370
}
371

372
.amplify-interactions-response-timestamp {
373
  color: #828282;
374
  font-size: 10px;
375
  letter-spacing: 0.5px;
376
  line-height: 16px;
377
  margin-left: 1.5em;
378
}
379

380
.amplify-interactions-response {
381
  padding: 1em;
382
  margin: 1em;
383
  width: 75%;
384
  border-radius: 20px 20px 20px 0;
385
  background-color: #dbdbdb;
386
  box-shadow: 1px 2px 4px 0 rgba(0, 0, 0, 0.1);
387
  font-size: 13px;
388
  line-height: 16px;
389
  color: #4a4a4a;
390
}
391

392
@media (min-width: 320px) and (max-width: 480px) {
393
  .amplify-interactions {
394
    width: var(--component-width-mobile);
395
  }
396
  .amplify-interactions-container {
397
    width: 85%;
398
  }
399
}
400
</style>

Read our documentation on viewing source code .

Loading