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;
|
||||
|
||||
@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 settings: HTMLDivElement;
|
||||
let recognition: any = null;
|
||||
let recording = false;
|
||||
|
||||
const settingsMap: Settings[] = [
|
||||
{
|
||||
key: "temperature",
|
||||
|
@ -55,7 +58,21 @@
|
|||
const token_price = 0.000002; // $0.002 per 1000 tokens
|
||||
|
||||
// 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
|
||||
afterUpdate(() => {
|
||||
|
@ -135,13 +152,14 @@
|
|||
return response;
|
||||
};
|
||||
|
||||
const submitForm = async (): Promise<void> => {
|
||||
const submitForm = async (recorded: boolean = false): Promise<void> => {
|
||||
// Compose the input message
|
||||
const inputMessage: Message = { role: "user", content: input.value };
|
||||
addMessage(chatId, inputMessage);
|
||||
|
||||
// Clear the input value
|
||||
input.value = "";
|
||||
input.blur();
|
||||
|
||||
// Resize back to single line height
|
||||
input.style.height = "auto";
|
||||
|
@ -157,6 +175,11 @@
|
|||
response.choices.map((choice) => {
|
||||
choice.message.usage = response.usage;
|
||||
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 = "";
|
||||
});
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<nav class="level chat-header">
|
||||
|
@ -326,7 +372,7 @@
|
|||
<form class="field has-addons has-addons-right" on:submit|preventDefault={submitForm}>
|
||||
<p class="control is-expanded">
|
||||
<textarea
|
||||
class="input is-info is-medium is-focused chat-input"
|
||||
class="input is-info is-focused chat-input"
|
||||
placeholder="Type your message here..."
|
||||
rows="1"
|
||||
on:keydown={(e) => {
|
||||
|
@ -337,19 +383,25 @@
|
|||
}
|
||||
}}
|
||||
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";
|
||||
}}
|
||||
bind:this={input}
|
||||
/>
|
||||
</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">
|
||||
<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
|
||||
>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-info is-medium" type="submit">Send</button>
|
||||
<button class="button is-info" type="submit">Send</button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
|
|
Loading…
Reference in New Issue