Add speech recognition + TTS support
This commit is contained in:
parent
ac9ca6e47e
commit
be8f54ebcc
17
src/app.scss
17
src/app.scss
|
@ -60,3 +60,20 @@ $fullhd: 2000px;
|
||||||
$modal-content-width: 1000px;
|
$modal-content-width: 1000px;
|
||||||
|
|
||||||
@import "/node_modules/bulma/bulma.sass";
|
@import "/node_modules/bulma/bulma.sass";
|
||||||
|
|
||||||
|
/* Pulsing effect - background goes to red color and back */
|
||||||
|
.is-pulse {
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
background-color: $info-light; /* Green */
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: $danger-light; /* Red */
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: $info-light /* Green */
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,9 @@
|
||||||
|
|
||||||
let input: HTMLTextAreaElement;
|
let input: HTMLTextAreaElement;
|
||||||
let settings: HTMLDivElement;
|
let settings: HTMLDivElement;
|
||||||
|
let recognition: any = null;
|
||||||
|
let recording = false;
|
||||||
|
|
||||||
const settingsMap: Settings[] = [
|
const settingsMap: Settings[] = [
|
||||||
{
|
{
|
||||||
key: "temperature",
|
key: "temperature",
|
||||||
|
@ -55,7 +58,21 @@
|
||||||
const token_price = 0.000002; // $0.002 per 1000 tokens
|
const token_price = 0.000002; // $0.002 per 1000 tokens
|
||||||
|
|
||||||
// Focus the input on mount
|
// Focus the input on mount
|
||||||
onMount(() => input.focus());
|
onMount(() => {
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
// Try to detect speech recognition support
|
||||||
|
if ("SpeechRecognition" in window) {
|
||||||
|
recognition = new SpeechRecognition();
|
||||||
|
} else if ("webkitSpeechRecognition" in window) {
|
||||||
|
recognition = new webkitSpeechRecognition();
|
||||||
|
} else {
|
||||||
|
console.log("Speech recognition not supported");
|
||||||
|
recognition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
recognition!.interimResults = false;
|
||||||
|
});
|
||||||
|
|
||||||
// Scroll to the bottom of the chat on update
|
// Scroll to the bottom of the chat on update
|
||||||
afterUpdate(() => {
|
afterUpdate(() => {
|
||||||
|
@ -135,13 +152,14 @@
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitForm = async (): Promise<void> => {
|
const submitForm = async (recorded: boolean = false): Promise<void> => {
|
||||||
// Compose the input message
|
// Compose the input message
|
||||||
const inputMessage: Message = { role: "user", content: input.value };
|
const inputMessage: Message = { role: "user", content: input.value };
|
||||||
addMessage(chatId, inputMessage);
|
addMessage(chatId, inputMessage);
|
||||||
|
|
||||||
// Clear the input value
|
// Clear the input value
|
||||||
input.value = "";
|
input.value = "";
|
||||||
|
input.blur();
|
||||||
|
|
||||||
// Resize back to single line height
|
// Resize back to single line height
|
||||||
input.style.height = "auto";
|
input.style.height = "auto";
|
||||||
|
@ -157,6 +175,11 @@
|
||||||
response.choices.map((choice) => {
|
response.choices.map((choice) => {
|
||||||
choice.message.usage = response.usage;
|
choice.message.usage = response.usage;
|
||||||
addMessage(chatId, choice.message);
|
addMessage(chatId, choice.message);
|
||||||
|
// Use TTS to read the response, if query was recorded
|
||||||
|
if (recorded && "SpeechSynthesisUtterance" in window) {
|
||||||
|
const utterance = new SpeechSynthesisUtterance(choice.message.content);
|
||||||
|
speechSynthesis.speak(utterance);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -206,6 +229,29 @@
|
||||||
input.value = "";
|
input.value = "";
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const recordToggle = () => {
|
||||||
|
// Check if already recording - if so, stop
|
||||||
|
if (recording) {
|
||||||
|
recognition?.stop();
|
||||||
|
recording = false;
|
||||||
|
} else {
|
||||||
|
// Mark as recording
|
||||||
|
recording = true;
|
||||||
|
|
||||||
|
// Start speech recognition
|
||||||
|
recognition!.onresult = (event) => {
|
||||||
|
// Stop speech recognition, submit the form and remove the pulse
|
||||||
|
const last = event.results.length - 1;
|
||||||
|
const text = event.results[last][0].transcript;
|
||||||
|
input.value = text;
|
||||||
|
recognition.stop();
|
||||||
|
recording = false;
|
||||||
|
submitForm(true);
|
||||||
|
};
|
||||||
|
recognition?.start();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="level chat-header">
|
<nav class="level chat-header">
|
||||||
|
@ -326,7 +372,7 @@
|
||||||
<form class="field has-addons has-addons-right" on:submit|preventDefault={submitForm}>
|
<form class="field has-addons has-addons-right" on:submit|preventDefault={submitForm}>
|
||||||
<p class="control is-expanded">
|
<p class="control is-expanded">
|
||||||
<textarea
|
<textarea
|
||||||
class="input is-info is-medium is-focused chat-input"
|
class="input is-info is-focused chat-input"
|
||||||
placeholder="Type your message here..."
|
placeholder="Type your message here..."
|
||||||
rows="1"
|
rows="1"
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
|
@ -337,19 +383,25 @@
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
// Resize the textarea to fit the content
|
// Resize the textarea to fit the content - auto is important to reset the height after deleting content
|
||||||
|
input.style.height = "auto";
|
||||||
input.style.height = input.scrollHeight + "px";
|
input.style.height = input.scrollHeight + "px";
|
||||||
}}
|
}}
|
||||||
bind:this={input}
|
bind:this={input}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
<p class="control" class:is-hidden={!recognition}>
|
||||||
|
<button class="button is-info is-light" class:is-pulse={recording} on:click|preventDefault={recordToggle}
|
||||||
|
><span class="greyscale">🎤</span></button
|
||||||
|
>
|
||||||
|
</p>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button is-link is-light is-medium" on:click|preventDefault={showSettings}
|
<button class="button is-link is-light" on:click|preventDefault={showSettings}
|
||||||
><span class="greyscale">⚙️</span></button
|
><span class="greyscale">⚙️</span></button
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button is-info is-medium" type="submit">Send</button>
|
<button class="button is-info" type="submit">Send</button>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue