Skip to content

Commit ff9df4c

Browse files
authored
Merge pull request #243 from Deep-Blue-2013/master
Add Web Speech #242
2 parents 3655935 + 5921701 commit ff9df4c

File tree

15 files changed

+165
-9
lines changed

15 files changed

+165
-9
lines changed

src/web-live-chat/src/lib/helpers/typedefs.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ IRichContent.prototype.text;
118118
* @param {ChatResponseModel} message
119119
*/
120120

121+
/**
122+
* Invoked when speech to text is detected.
123+
*
124+
* @callback OnSpeechToTextDetected
125+
* @param {string} text
126+
*/
127+
128+
121129
// having to export an empty object here is annoying,
122130
// but required for vscode to pass on your types.
123131
export default {};

src/web-live-chat/src/lib/services/agent-service.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { agentListUrl, agentDetailUrl } from '$lib/services/api-endpoints.js';
2+
import { setAuthorization } from '$lib/helpers/http';
23
import axios from 'axios';
34

45
/**
56
* @returns {Promise<import('$typedefs').AgentModel[]>}
67
*/
78
export async function getAgents() {
9+
setAuthorization();
810
const response = await axios.get(agentListUrl);
911
return response.data;
1012
}
@@ -14,6 +16,7 @@ export async function getAgents() {
1416
* @returns {Promise<import('$typedefs').AgentModel>}
1517
*/
1618
export async function getAgent(id) {
19+
setAuthorization();
1720
let url = agentDetailUrl.replace("{id}", id);
1821
const response = await axios.get(url);
1922
return response.data;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API
2+
const SpeechRecognition =
3+
window.SpeechRecognition || window.webkitSpeechRecognition;
4+
const SpeechRecognitionEvent =
5+
window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
6+
7+
const recognition = new SpeechRecognition();
8+
recognition.continuous = false;
9+
recognition.lang = "en-US";
10+
recognition.interimResults = false;
11+
recognition.maxAlternatives = 1;
12+
13+
const synth = window.speechSynthesis;
14+
15+
16+
const utterThis = new SpeechSynthesisUtterance();
17+
utterThis.pitch = 1;
18+
utterThis.rate = 1;
19+
20+
export const webSpeech = {
21+
/** @type {import('$typedefs').OnSpeechToTextDetected} */
22+
onSpeechToTextDetected: () => {},
23+
24+
start() {
25+
recognition.start();
26+
console.log("Ready to receive a voice command.");
27+
},
28+
29+
/** @param {string} transcript */
30+
utter(transcript) {
31+
setVoiceSynthesis();
32+
utterThis.text = transcript
33+
synth.speak(utterThis);
34+
}
35+
}
36+
37+
function setVoiceSynthesis() {
38+
if (utterThis.voice == null) {
39+
const voices = synth.getVoices();
40+
for (let i = 0; i < voices.length; i++) {
41+
if (voices[i].name === "Microsoft Michelle Online (Natural) - English (United States)") {
42+
utterThis.voice = voices[i];
43+
break;
44+
}
45+
}
46+
}
47+
}
48+
49+
recognition.onresult = (event) => {
50+
const text = event.results[0][0].transcript;
51+
console.log(`Confidence: ${text} ${event.results[0][0].confidence}`);
52+
webSpeech.onSpeechToTextDetected(text);
53+
};
54+
55+
recognition.onnomatch = (event) => {
56+
console.log("I didn't recognize that color.");
57+
};
58+
59+
recognition.onerror = (event) => {
60+
console.log(`Error occurred in recognition: ${event.error}`);
61+
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="row" style="margin-top:150px;margin-left:25px;margin-right:25px;">
22
<div class="col-lg-8 offset-lg-2">
33
<p class="section-subtitle text-muted text-center pt-4 font-secondary">We craft digital, graphic and dimensional thinking, to create category leading brand experiences that have meaning and add a value for our clients.</p>
4-
<div class="d-flex justify-content-center"><a href="/login" class="btn btn-primary">New Conversation</a></div>
4+
<div class="d-flex justify-content-center"><a href="/login" class="btn btn-primary">Get Started</a></div>
55
</div>
66
</div>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<script>
2+
import { Container, Row, Col } from "sveltestrap";
3+
4+
// This page is used to initialize a new conversation for client
5+
import { page } from '$app/stores';
6+
import { onMount } from 'svelte';
7+
import { getAgents } from '$lib/services/agent-service.js'
8+
9+
const params = $page.params;
10+
let agentId = "undefined";
11+
/** @type {import('$typedefs').AgentModel[]} */
12+
let agents = [];
13+
14+
onMount(async () => {
15+
agents = await getAgents();
16+
agentId = agents[0].id;
17+
});
18+
</script>
19+
20+
<Container fluid>
21+
<Row>
22+
<div class="col-12">
23+
<div style="margin-top: 10vh; margin-left:10vw;">
24+
{#each agents as agent}
25+
<div>
26+
<input
27+
class="form-check-input m-1"
28+
type="radio"
29+
name="agents"
30+
id={agent.id}
31+
value={agent.id}
32+
checked = {agentId == agent.id}
33+
on:click={() => agentId = agent.id}
34+
/>
35+
<label class="form-check-label" for={agent.id}>
36+
{agent.name}
37+
</label>
38+
<div class="mx-4">{agent.description}</div>
39+
</div>
40+
{/each}
41+
</div>
42+
</div>
43+
</Row>
44+
<Row class="text-center">
45+
<Col>
46+
<p class="section-subtitle text-muted text-center pt-4 font-secondary">We craft digital, graphic and dimensional thinking, to create category leading brand experiences that have meaning and add a value for our clients.</p>
47+
<div class="d-flex justify-content-center">
48+
<a href="/chat/{agentId}" class="btn btn-primary">
49+
<i class="mdi mdi-chat" />
50+
<span>Start Conversation</span>
51+
</a>
52+
</div>
53+
</Col>
54+
</Row>
55+
</Container>

src/web-live-chat/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import { onMount } from 'svelte';
1616
import Link from 'svelte-link';
1717
import { signalr } from '$lib/services/signalr-service.js';
18+
import { webSpeech } from '$lib/services/web-speech.js';
1819
import { sendMessageToHub, GetDialogs } from '$lib/services/conversation-service.js';
1920
import { format } from '$lib/helpers/datetime';
2021
import RcText from './rc-text.svelte';
@@ -43,6 +44,7 @@
4344
4445
// @ts-ignore
4546
let scrollbar;
47+
let microphoneIcon = "microphone-off";
4648
4749
/** @type {import('$typedefs').ChatResponseModel[]} */
4850
let dialogs = [];
@@ -78,17 +80,36 @@
7880
7981
/** @param {import('$typedefs').ChatResponseModel} message */
8082
function onMessageReceivedFromAssistant(message) {
81-
dialogs.push(message);
82-
refresh();
83+
webSpeech.utter(message.text);
84+
// clean rich content elements
85+
dialogs.forEach(dialog => {
86+
if (dialog.rich_content.messaging_type == "quick_reply") {
87+
dialog.rich_content.quick_repies = [];
88+
}
89+
});
90+
91+
dialogs.push(message);
92+
refresh();
8393
}
8494
85-
async function sendMessage() {
95+
async function sendTextMessage() {
8696
await sendMessageToHub(params.agentId, params.conversationId, text);
8797
}
8898
99+
async function startListen() {
100+
microphoneIcon = "microphone";
101+
webSpeech.onSpeechToTextDetected = async (transcript) => {
102+
text = transcript;
103+
await sendTextMessage();
104+
microphoneIcon = "microphone-off";
105+
}
106+
webSpeech.start();
107+
}
108+
89109
function refresh() {
90110
// trigger UI render
91111
dialogs = dialogs;
112+
92113
setTimeout(() => {
93114
const { viewport } = scrollbar.elements();
94115
viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'smooth' }); // set scroll offset
@@ -194,8 +215,16 @@
194215

195216
<div class="p-3 chat-input-section" style="height: 10vh">
196217
<div class="row">
218+
<div class="col-auto">
219+
<button
220+
type="submit"
221+
class="btn btn-primary btn-rounded waves-effect waves-light"
222+
on:click={startListen}>
223+
<i class="mdi mdi-{microphoneIcon} md-36" />
224+
</button>
225+
</div>
197226
<div class="col">
198-
<div class="position-relative">
227+
<div class="position-relative">
199228
<input type="text" class="form-control chat-input" bind:value={text} placeholder="Enter Message..." />
200229
<div class="chat-input-links" id="tooltip-container">
201230
<ul class="list-inline mb-0">
@@ -210,10 +239,10 @@
210239
<button
211240
type="submit"
212241
class="btn btn-primary btn-rounded chat-send w-md waves-effect waves-light"
213-
on:click={sendMessage}
242+
on:click={sendTextMessage}
214243
><span class="d-none d-sm-inline-block me-2">Send</span>
215-
<i class="mdi mdi-send" /></button
216-
>
244+
<i class="mdi mdi-send" />
245+
</button>
217246
</div>
218247
</div>
219248
</div>

src/web-live-chat/src/routes/login/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
isOpen = true;
2929
msg = 'Authentication success';
3030
status = 'success';
31-
goto(`/chat/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a`);
31+
goto(`/chat`);
3232
});
3333
}
3434
</script>
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)