@broadreachalliance/q-assistant
Version:
A React-based chat window supporting both voice and text communication modes.
134 lines • 4.81 kB
JavaScript
import { useEffect, useState, useRef } from "react";
import axios from "axios";
import config from "../config";
const useAssistant = () => {
const finalTranscriptRef = useRef("");
const speechRecognitionref = useRef(null);
const availableVoicesref = useRef([]);
const [state, setState] = useState("idle"); // idle | listening | responding | processing
const timeoutRef = useRef(null);
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
useEffect(() => {
const loadVoices = () => {
const voices = window.speechSynthesis.getVoices();
availableVoicesref.current = voices;
};
const initInteractions = () => {
const sttSupported = "SpeechRecognition" in window || "webkitSpeechRecognition" in window;
const ttsSupported = "speechSynthesis" in window;
if (ttsSupported) {
window.speechSynthesis.addEventListener("voiceschanged", loadVoices);
}
if (sttSupported) {
speechRecognitionref.current = new SpeechRecognition();
speechRecognitionref.current.continuous = true;
speechRecognitionref.current.interimResults = true;
speechRecognitionref.current.lang = "en-US";
speechRecognitionref.current.onresult = event => {
handleRecognitionResult(event);
};
speechRecognitionref.current.onerror = event => {
console.error("Speech recognition error", event.error);
};
} else {
console.error("Speech recognition not supported in this browser.");
}
return () => {
if (speechRecognitionref.current) {
speechRecognitionref.current.stop();
}
};
};
initInteractions();
return () => {
window.speechSynthesis.removeEventListener("voiceschanged", loadVoices);
};
}, []);
const startListening = () => {
if (!localStorage.getItem("token") || !localStorage.getItem(config.USER_DETAIL_STORAGE_KEY) || !process.env.REACT_APP_AI_ASSISTANT_HOST) {
speakText("Unable to process your request.");
stopListening();
return null;
}
if (speechRecognitionref.current) {
speechRecognitionref.current.start();
setState("listening");
}
};
const stopListening = () => {
if (speechRecognitionref.current) {
speechRecognitionref.current.stop();
setState("idle");
}
};
const handleRecognitionResult = event => {
for (let i = event.resultIndex; i < event.results.length; i++) {
finalTranscriptRef.current = event.results[i][0].transcript;
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
sendMessage();
}, 2000);
};
const sendMessage = async () => {
const messageToSend = finalTranscriptRef.current;
if (!messageToSend.trim()) return;
stopListening();
finalTranscriptRef.current = "";
const userData = localStorage.getItem(config.USER_DETAIL_STORAGE_KEY);
try {
setState("processing");
const response = await axios.post(process.env.REACT_APP_AI_ASSISTANT_HOST, {
messages: messageToSend,
thread_id: JSON.parse(userData)?.userID ?? "new_user",
debug: false
}, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
"Content-Type": "application/json"
}
});
let lines = response.data.response;
lines = lines.replace(/\s*\(.*?\)\s*/g, " ").trim();
lines = lines.replaceAll("**", " ");
lines = lines.replace(/\b([aApP])\.(m|M)\b/g, "$1$2");
speechRecognitionref.current?.stop();
speakText(lines);
} catch (error) {
console.error("Error:", error);
speakText("Sorry, there was an error processing your message.");
}
};
const speakText = text => {
text = text.replace(/\s*\(.*?\)\s*/g, ' ').trim();
if (!window.speechSynthesis) return;
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
const selectedVoice = localStorage.getItem("selectedVoice");
if (selectedVoice) {
utterance.voice = availableVoicesref.current.find(voice => voice.name === selectedVoice);
} else {
utterance.voice = availableVoicesref.current?.[0];
}
utterance.rate = process.env.REACT_APP_SPEECH_RATE;
utterance.onstart = () => {
setState("responding");
};
utterance.onend = () => {
if (localStorage.getItem("token") && localStorage.getItem(config.USER_DETAIL_STORAGE_KEY) && process.env.REACT_APP_AI_ASSISTANT_HOST) {
startListening();
} else {
setState("idle");
}
};
window.speechSynthesis.speak(utterance);
};
return {
startListening,
stopListening,
state
};
};
export default useAssistant;