File size: 4,606 Bytes
f8b5d42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | import { useEffect, useCallback, useRef } from "react";
import { Microphone } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip";
import _regeneratorRuntime from "regenerator-runtime";
import SpeechRecognition, {
useSpeechRecognition,
} from "react-speech-recognition";
import { PROMPT_INPUT_EVENT } from "../../PromptInput";
import { useTranslation } from "react-i18next";
import Appearance from "@/models/appearance";
let timeout;
const SILENCE_INTERVAL = 3_200; // wait in seconds of silence before closing.
/**
* Speech-to-text input component for the chat window.
* @param {Object} props - The component props
* @param {(textToAppend: string, autoSubmit: boolean) => void} props.sendCommand - The function to send the command
* @returns {React.ReactElement} The SpeechToText component
*/
export default function SpeechToText({ sendCommand }) {
const previousTranscriptRef = useRef("");
const {
transcript,
listening,
resetTranscript,
browserSupportsSpeechRecognition,
browserSupportsContinuousListening,
isMicrophoneAvailable,
} = useSpeechRecognition({
clearTranscriptOnListen: true,
});
const { t } = useTranslation();
function startSTTSession() {
if (!isMicrophoneAvailable) {
alert(
"AnythingLLM does not have access to microphone. Please enable for this site to use this feature."
);
return;
}
resetTranscript();
previousTranscriptRef.current = "";
SpeechRecognition.startListening({
continuous: browserSupportsContinuousListening,
language: window?.navigator?.language ?? "en-US",
});
}
function endSTTSession() {
SpeechRecognition.stopListening();
// If auto submit is enabled, send an empty string to the chat window to submit the current transcript
// since every chunk of text should have been streamed to the chat window by now.
if (Appearance.get("autoSubmitSttInput")) {
sendCommand({
text: "",
autoSubmit: true,
writeMode: "append",
});
}
resetTranscript();
previousTranscriptRef.current = "";
clearTimeout(timeout);
}
const handleKeyPress = useCallback(
(event) => {
// CTRL + m on Mac and Windows to toggle STT listening
if (event.ctrlKey && event.keyCode === 77) {
if (listening) {
endSTTSession();
} else {
startSTTSession();
}
}
},
[listening, endSTTSession, startSTTSession]
);
function handlePromptUpdate(e) {
if (!e?.detail && timeout) {
endSTTSession();
clearTimeout(timeout);
}
}
useEffect(() => {
document.addEventListener("keydown", handleKeyPress);
return () => {
document.removeEventListener("keydown", handleKeyPress);
};
}, [handleKeyPress]);
useEffect(() => {
if (!!window)
window.addEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);
return () =>
window?.removeEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);
}, []);
useEffect(() => {
if (transcript?.length > 0 && listening) {
const previousTranscript = previousTranscriptRef.current;
const newContent = transcript.slice(previousTranscript.length);
// Stream just the diff of the new content since transcript is an accumulating string.
// and not just the new content transcribed.
if (newContent.length > 0)
sendCommand({ text: newContent, writeMode: "append" });
previousTranscriptRef.current = transcript;
clearTimeout(timeout);
timeout = setTimeout(() => {
endSTTSession();
}, SILENCE_INTERVAL);
}
}, [transcript, listening]);
if (!browserSupportsSpeechRecognition) return null;
return (
<div
data-tooltip-id="tooltip-microphone-btn"
data-tooltip-content={`${t("chat_window.microphone")} (CTRL + M)`}
aria-label={t("chat_window.microphone")}
onClick={listening ? endSTTSession : startSTTSession}
className={`border-none relative flex justify-center items-center opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 cursor-pointer ${
!!listening ? "!opacity-100" : ""
}`}
>
<Microphone
weight="fill"
color="var(--theme-sidebar-footer-icon-fill)"
className={`w-[22px] h-[22px] pointer-events-none text-theme-text-primary ${
listening ? "animate-pulse-glow" : ""
}`}
/>
<Tooltip
id="tooltip-microphone-btn"
place="top"
delayShow={300}
className="tooltip !text-xs z-99"
/>
</div>
);
}
|